From a6f7fc24d0000155556d9bd1e999aa3fb4fc1404 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Mon, 25 May 2026 14:45:47 +0800 Subject: [PATCH 01/19] feat(omp): add Oh My Pi platform support (Phase 1 + Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .omp/ dogfood directory with commands, skills, agents, and extension - Register OMP in AI_TOOLS registry (ai-tools.ts) with extension-backed pattern - Add --omp CLI flag and InitOptions field - Create configurator (omp.ts) with configureOmp + collectOmpTemplates - Add template module (templates/omp/) with agents and extension source - Register in PLATFORM_FUNCTIONS with collectTemplates for update tracking - Update cli_adapter.py: Platform type, detect_platform, config_dir_name, etc. - Update task_store.py: add .omp to _SUBAGENT_CONFIG_DIRS - Add 'Oh My Pi' to workflow.md platform blocks - Update README.md and README_CN.md platform lists - Add omp.test.ts (template tests) and regression.test.ts entries Extension implements session_start, before_agent_start, and input handlers with TurnContextCache (1.5s TTL) for workflow-state + task context injection. No tool_call/subagent handlers needed — OMP native task tool covers both. --- .omp/agents/trellis-check.md | 41 + .omp/agents/trellis-implement.md | 44 + .omp/agents/trellis-research.md | 29 + .omp/commands/trellis-continue.md | 55 ++ .omp/commands/trellis-finish-work.md | 66 ++ .omp/extensions/trellis/index.ts | 292 +++++++ .omp/skills/trellis-before-dev/SKILL.md | 34 + .omp/skills/trellis-brainstorm/SKILL.md | 548 ++++++++++++ .omp/skills/trellis-break-loop/SKILL.md | 130 +++ .omp/skills/trellis-check/SKILL.md | 92 +++ .omp/skills/trellis-meta/SKILL.md | 73 ++ .../add-project-local-conventions.md | 83 ++ .../customize-local/change-agents.md | 54 ++ .../customize-local/change-context-loading.md | 81 ++ .../customize-local/change-hooks.md | 57 ++ .../change-skills-or-commands.md | 78 ++ .../customize-local/change-spec-structure.md | 83 ++ .../customize-local/change-task-lifecycle.md | 90 ++ .../customize-local/change-workflow.md | 64 ++ .../references/customize-local/overview.md | 55 ++ .../local-architecture/context-injection.md | 68 ++ .../local-architecture/generated-files.md | 80 ++ .../references/local-architecture/overview.md | 51 ++ .../local-architecture/spec-system.md | 102 +++ .../local-architecture/task-system.md | 101 +++ .../references/local-architecture/workflow.md | 75 ++ .../local-architecture/workspace-memory.md | 71 ++ .../references/platform-files/agents.md | 79 ++ .../platform-files/hooks-and-settings.md | 69 ++ .../references/platform-files/overview.md | 59 ++ .../references/platform-files/platform-map.md | 91 ++ .../platform-files/skills-and-commands.md | 83 ++ .omp/skills/trellis-spec-bootstarp/SKILL.md | 41 + .../references/mcp-setup.md | 90 ++ .../references/repository-analysis.md | 59 ++ .../references/spec-task-planning.md | 61 ++ .../references/spec-writing.md | 70 ++ .omp/skills/trellis-update-spec/SKILL.md | 356 ++++++++ .trellis/scripts/common/cli_adapter.py | 37 +- .trellis/scripts/common/task_store.py | 1 + .../check.jsonl | 5 + .../implement.jsonl | 7 + .../prd.md | 278 +++++++ .../task.json | 26 + .trellis/workflow.md | 16 +- README.md | 4 +- README_CN.md | 4 +- packages/cli/src/configurators/omp.ts | 86 ++ .../references/platform-files/platform-map.md | 17 + .../src/templates/omp/agents/trellis-check.md | 41 + .../templates/omp/agents/trellis-implement.md | 44 + .../templates/omp/agents/trellis-research.md | 29 + .../omp/extensions/trellis/index.ts.txt | 292 +++++++ packages/cli/src/templates/omp/index.ts | 12 + .../trellis/scripts/common/cli_adapter.py | 37 +- .../trellis/scripts/common/task_store.py | 1 + .../cli/src/templates/trellis/workflow.md | 6 +- packages/cli/src/types/ai-tools.ts | 781 +++++++++--------- packages/cli/test/regression.test.ts | 16 + packages/cli/test/templates/omp.test.ts | 54 ++ 60 files changed, 5051 insertions(+), 398 deletions(-) create mode 100644 .omp/agents/trellis-check.md create mode 100644 .omp/agents/trellis-implement.md create mode 100644 .omp/agents/trellis-research.md create mode 100644 .omp/commands/trellis-continue.md create mode 100644 .omp/commands/trellis-finish-work.md create mode 100644 .omp/extensions/trellis/index.ts create mode 100644 .omp/skills/trellis-before-dev/SKILL.md create mode 100644 .omp/skills/trellis-brainstorm/SKILL.md create mode 100644 .omp/skills/trellis-break-loop/SKILL.md create mode 100644 .omp/skills/trellis-check/SKILL.md create mode 100644 .omp/skills/trellis-meta/SKILL.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/add-project-local-conventions.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-agents.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-context-loading.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-hooks.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-skills-or-commands.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-spec-structure.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-task-lifecycle.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/change-workflow.md create mode 100644 .omp/skills/trellis-meta/references/customize-local/overview.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/context-injection.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/generated-files.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/overview.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/spec-system.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/task-system.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/workflow.md create mode 100644 .omp/skills/trellis-meta/references/local-architecture/workspace-memory.md create mode 100644 .omp/skills/trellis-meta/references/platform-files/agents.md create mode 100644 .omp/skills/trellis-meta/references/platform-files/hooks-and-settings.md create mode 100644 .omp/skills/trellis-meta/references/platform-files/overview.md create mode 100644 .omp/skills/trellis-meta/references/platform-files/platform-map.md create mode 100644 .omp/skills/trellis-meta/references/platform-files/skills-and-commands.md create mode 100644 .omp/skills/trellis-spec-bootstarp/SKILL.md create mode 100644 .omp/skills/trellis-spec-bootstarp/references/mcp-setup.md create mode 100644 .omp/skills/trellis-spec-bootstarp/references/repository-analysis.md create mode 100644 .omp/skills/trellis-spec-bootstarp/references/spec-task-planning.md create mode 100644 .omp/skills/trellis-spec-bootstarp/references/spec-writing.md create mode 100644 .omp/skills/trellis-update-spec/SKILL.md create mode 100644 .trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl create mode 100644 .trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl create mode 100644 .trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md create mode 100644 .trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json create mode 100644 packages/cli/src/configurators/omp.ts create mode 100644 packages/cli/src/templates/omp/agents/trellis-check.md create mode 100644 packages/cli/src/templates/omp/agents/trellis-implement.md create mode 100644 packages/cli/src/templates/omp/agents/trellis-research.md create mode 100644 packages/cli/src/templates/omp/extensions/trellis/index.ts.txt create mode 100644 packages/cli/src/templates/omp/index.ts create mode 100644 packages/cli/test/templates/omp.test.ts diff --git a/.omp/agents/trellis-check.md b/.omp/agents/trellis-check.md new file mode 100644 index 000000000..098265f9a --- /dev/null +++ b/.omp/agents/trellis-check.md @@ -0,0 +1,41 @@ +--- +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, search, ast_grep, lsp +--- + +# 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 via the `task` tool. +- If injected workflow-state breadcrumbs 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 and follow the spec and research files listed in the task's `check.jsonl`. +3. Review all changed code against the task PRD and project specs. +4. Fix issues directly when they are within scope. +5. Run the relevant lint, typecheck, and focused tests 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/.omp/agents/trellis-implement.md b/.omp/agents/trellis-implement.md new file mode 100644 index 000000000..a022524c2 --- /dev/null +++ b/.omp/agents/trellis-implement.md @@ -0,0 +1,44 @@ +--- +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, search, ast_grep, lsp +--- + +# 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 via the `task` tool. +- If injected workflow-state breadcrumbs 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 and follow the spec and research files listed in the task's `implement.jsonl`. +3. Implement the requested change using existing project patterns. +4. Run the relevant lint, typecheck, and focused tests for the touched code. +5. 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/.omp/agents/trellis-research.md b/.omp/agents/trellis-research.md new file mode 100644 index 000000000..372167bd2 --- /dev/null +++ b/.omp/agents/trellis-research.md @@ -0,0 +1,29 @@ +--- +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, search, web_search +--- + +# 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 `/research/` when it does not exist. +3. Search internal code, specs, and relevant external documentation. +4. Write each distinct topic to `/research/.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/.omp/commands/trellis-continue.md b/.omp/commands/trellis-continue.md new file mode 100644 index 000000000..3a60538f0 --- /dev/null +++ b/.omp/commands/trellis-continue.md @@ -0,0 +1,55 @@ +# Continue Current Task + +Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. + +--- + +## Step 1: Load Current Context + +```bash +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: + +- `status=planning` + no `prd.md` → **1.1** (load `trellis-brainstorm`) +- `status=planning` + `prd.md` exists + `implement.jsonl` not curated (only the seed `_example` row) → **1.3** +- `status=planning` + `prd.md` + curated `implement.jsonl` → **1.4** (run `task.py start` to enter Phase 2) +- `status=in_progress` + implementation not started → **2.1** +- `status=in_progress` + implementation done, not yet checked → **2.2** +- `status=in_progress` + check passed → **3.1** +- `status=completed` (rare; usually archived immediately) → archive flow + +Phase rules (full detail in `.trellis/workflow.md`): + +1. Run steps **in order** within a phase — `[required]` steps must not be skipped +2. `[once]` steps are already done if the output exists (e.g., `prd.md` for 1.1; `implement.jsonl` with curated entries for 1.3) — skip them +3. You may go back to an earlier phase if discoveries require it + +## Step 4: Load the Specific Step + +Once you know which step to resume at: + +```bash +python3 ./.trellis/scripts/get_context.py --mode phase --step --platform omp +``` + +Follow the loaded instructions. After each `[required]` step completes, move to the next. + +--- + +## Reference + +Full workflow, skill routing table, and the DO-NOT-skip table live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. diff --git a/.omp/commands/trellis-finish-work.md b/.omp/commands/trellis-finish-work.md new file mode 100644 index 000000000..ab751c687 --- /dev/null +++ b/.omp/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: ``. 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: ``." +- **Genuinely unsure** — ask the user once: "Are `` 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 +``` + +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: `` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts new file mode 100644 index 000000000..7a5bb1a04 --- /dev/null +++ b/.omp/extensions/trellis/index.ts @@ -0,0 +1,292 @@ +import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { spawnSync } from "node:child_process"; + +// --------------------------------------------------------------------------- +// Project root detection +// --------------------------------------------------------------------------- + +function findProjectRoot(startDir: string): string | null { + let current = startDir; + while (true) { + if (existsSync(join(current, ".trellis"))) return current; + const parent = dirname(current); + if (parent === current) break; + current = parent; + } + return null; +} + +// --------------------------------------------------------------------------- +// Active task resolution +// --------------------------------------------------------------------------- + +function resolveActiveTaskStatus( + projectRoot: string, +): { status: string; taskDir: string | null } { + const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); + if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null }; + + let sessionFiles: string[]; + try { + sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); + } catch { + return { status: "no_task", taskDir: null }; + } + if (sessionFiles.length === 0) return { status: "no_task", taskDir: null }; + + if (sessionFiles.length > 1) { + sessionFiles.sort((a, b) => { + const ma = statSync(join(sessionsDir, a)).mtimeMs; + const mb = statSync(join(sessionsDir, b)).mtimeMs; + return mb - ma; + }); + } + + const sessionFile = sessionFiles[0]; + let sessionData: Record; + try { + sessionData = JSON.parse( + readFileSync(join(sessionsDir, sessionFile), "utf-8"), + ); + } catch { + return { status: "no_task", taskDir: null }; + } + + const currentTask = sessionData.current_task; + if (typeof currentTask !== "string" || !currentTask) + return { status: "no_task", taskDir: null }; + + const taskDir = join(projectRoot, currentTask); + const taskJsonPath = join(taskDir, "task.json"); + if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null }; + + let taskData: Record; + try { + taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8")); + } catch { + return { status: "no_task", taskDir: null }; + } + + return { + status: typeof taskData.status === "string" ? taskData.status : "planning", + taskDir, + }; +} + +// --------------------------------------------------------------------------- +// Task context — prd.md, info.md, and jsonl-referenced spec/research files +// --------------------------------------------------------------------------- + +function buildTaskContext(projectRoot: string, taskDir: string): string { + const parts: string[] = []; + + let prd = ""; + try { prd = readFileSync(join(taskDir, "prd.md"), "utf-8"); } catch { } + if (prd.trim()) parts.push(`## PRD\n\n${prd.trim()}`); + + let info = ""; + try { info = readFileSync(join(taskDir, "info.md"), "utf-8"); } catch { } + if (info.trim()) parts.push(`## Info\n\n${info.trim()}`); + + for (const jsonlName of ["implement.jsonl", "check.jsonl"]) { + const jsonlPath = join(taskDir, jsonlName); + if (!existsSync(jsonlPath)) continue; + + let lines: string[]; + try { + lines = readFileSync(jsonlPath, "utf-8").split(/\r?\n/); + } catch { + continue; + } + + const fileChunks: string[] = []; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const row = JSON.parse(trimmed) as Record; + const file = typeof row.file === "string" ? row.file.trim() : ""; + if (!file) continue; + let content = ""; + try { content = readFileSync(join(projectRoot, file), "utf-8"); } catch { } + if (content.trim()) { + fileChunks.push(`### ${file}\n\n${content.trim()}`); + } + } catch { + // seed rows and malformed lines are non-fatal + } + } + + if (fileChunks.length > 0) { + parts.push(`## ${jsonlName}\n\n${fileChunks.join("\n\n---\n\n")}`); + } + } + + return parts.length > 0 + ? `\n${parts.join("\n\n")}\n` + : ""; +} + +// --------------------------------------------------------------------------- +// Session overview — spawns get_context.py; non-fatal fallback on failure +// --------------------------------------------------------------------------- + +const SESSION_OVERVIEW_TIMEOUT_MS = 5000; +const SESSION_OVERVIEW_FALLBACK = + "Trellis workflow system active. Use skills and agents as directed by the workflow state."; + +function buildSessionOverview(projectRoot: string): string { + const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); + if (!existsSync(script)) return SESSION_OVERVIEW_FALLBACK; + + try { + const result = spawnSync("python3", [script, "--mode", "session-overview"], { + cwd: projectRoot, + encoding: "utf-8", + timeout: SESSION_OVERVIEW_TIMEOUT_MS, + windowsHide: true, + }); + if (result.status !== 0 || !result.stdout?.trim()) { + return SESSION_OVERVIEW_FALLBACK; + } + return result.stdout.trim(); + } catch { + return SESSION_OVERVIEW_FALLBACK; + } +} + +// --------------------------------------------------------------------------- +// Per-turn cache — prevents double-spawn when input + before_agent_start +// fire in the same turn +// --------------------------------------------------------------------------- + +class TurnContextCache { + private key: string | null = null; + private timestamp = 0; + private workflowMsg = ""; + private taskContext = ""; + private static readonly TTL_MS = 1500; + + get(projectRoot: string): { workflowMsg: string; taskContext: string } { + const now = Date.now(); + if ( + this.key === projectRoot && + now - this.timestamp < TurnContextCache.TTL_MS + ) { + return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + } + + const { status, taskDir } = resolveActiveTaskStatus(projectRoot); + + const workflowPath = join(projectRoot, ".trellis", "workflow.md"); + let workflowMd = ""; + try { workflowMd = readFileSync(workflowPath, "utf-8"); } catch { } + + let workflowBody = ""; + if (workflowMd) { + const blocks = parseWorkflowStateBlocks(workflowMd); + const activeBlock = blocks.find((b) => b.status === status); + if (activeBlock) { + workflowBody = `[workflow-state:${activeBlock.status}]\n${activeBlock.content}\n[/workflow-state:${activeBlock.status}]`; + } + } + if (!workflowBody) { + workflowBody = "Refer to workflow.md for current step."; + } + + const overview = buildSessionOverview(projectRoot); + this.workflowMsg = `\n${workflowBody}\n\n\n\n${overview}\n`; + this.taskContext = taskDir ? buildTaskContext(projectRoot, taskDir) : ""; + + this.key = projectRoot; + this.timestamp = now; + return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + } +} + +// --------------------------------------------------------------------------- +// Workflow-state tag parsing +// --------------------------------------------------------------------------- + +const WORKFLOW_STATE_RE = + /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g; + +interface WorkflowStateBlock { + status: string; + content: string; +} + +function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] { + const blocks: WorkflowStateBlock[] = []; + for (const match of markdown.matchAll(WORKFLOW_STATE_RE)) { + blocks.push({ + status: match[1], + content: match[2].trim(), + }); + } + return blocks; +} + +// --------------------------------------------------------------------------- +// Extension entry point +// --------------------------------------------------------------------------- + +export default function(pi: ExtensionAPI): void { + pi.setLabel("Trellis"); + + let projectRoot: string | null = null; + const turnCache = new TurnContextCache(); + + pi.on("session_start", async (_event, ctx) => { + projectRoot = findProjectRoot(ctx.cwd); + if (projectRoot) { + ctx.ui.notify("Trellis workflow system available", "info"); + } + }); + + pi.on("before_agent_start", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); + } + if (!projectRoot) return; + + const cached = turnCache.get(projectRoot); + + if (cached.taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: cached.taskContext, + display: false, + }); + } + + return { + message: { + customType: "trellis-workflow-state", + content: cached.workflowMsg, + display: false, + }, + }; + }); + + pi.on("input", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); + } + if (!projectRoot) return { action: "continue" }; + const cached = turnCache.get(projectRoot); + if (cached.workflowMsg) { + return { + action: "continue", + message: { + customType: "trellis-workflow-state", + content: cached.workflowMsg, + display: false, + }, + }; + } + return { action: "continue" }; + }); +} diff --git a/.omp/skills/trellis-before-dev/SKILL.md b/.omp/skills/trellis-before-dev/SKILL.md new file mode 100644 index 000000000..9c6ec9c72 --- /dev/null +++ b/.omp/skills/trellis-before-dev/SKILL.md @@ -0,0 +1,34 @@ +--- +name: trellis-before-dev +description: "Discovers and injects project-specific coding guidelines from .trellis/spec/ before implementation begins. Reads spec indexes, pre-development checklists, and shared thinking guides for the target package. Use when starting a new coding task, before writing any code, switching to a different package, or needing to refresh project conventions and standards." +--- + +Read the relevant development guidelines before starting your task. + +Execute these steps: + +1. **Discover packages and their spec layers**: + ```bash + python3 ./.trellis/scripts/get_context.py --mode packages + ``` + +2. **Identify which specs apply** to your task based on: + - Which package you're modifying (e.g., `cli/`, `docs-site/`) + - What type of work (backend, frontend, unit-test, docs, etc.) + +3. **Read the spec index** for each relevant module: + ```bash + cat .trellis/spec///index.md + ``` + Follow the **"Pre-Development Checklist"** section in the index. + +4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns. + +5. **Always read shared guides**: + ```bash + cat .trellis/spec/guides/index.md + ``` + +6. Understand the coding standards and patterns you need to follow, then proceed with your development plan. + +This step is **mandatory** before writing any code. diff --git a/.omp/skills/trellis-brainstorm/SKILL.md b/.omp/skills/trellis-brainstorm/SKILL.md new file mode 100644 index 000000000..670e19b02 --- /dev/null +++ b/.omp/skills/trellis-brainstorm/SKILL.md @@ -0,0 +1,548 @@ +--- +name: trellis-brainstorm +description: "Guides collaborative requirements discovery before implementation. Creates task directory, seeds PRD, asks high-value questions one at a time, researches technical choices, and converges on MVP scope. Use when requirements are unclear, there are multiple valid approaches, or the user describes a new feature or complex task." +--- + +# Brainstorm - Requirements Discovery (AI Coding Enhanced) + +**CoreRule**: Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. + +--- + +Guide AI through collaborative requirements discovery **before implementation**, optimized for AI coding workflows: + +* **Task-first** (capture ideas immediately) +* **Action-before-asking** (reduce low-value questions) +* **Research-first** for technical choices (avoid asking users to invent options) +* **Diverge → Converge** (expand thinking, then lock MVP) + +--- + +## When to Use + +Triggered from /trellis:start when the user describes a development task, especially when: + +* requirements are unclear or evolving +* there are multiple valid implementation paths +* trade-offs matter (UX, reliability, maintainability, cost, performance) +* the user might not know the best options up front + +--- + +## Core Principles (Non-negotiable) + +1. **Task-first (capture early)** + Always ensure a task exists at the start so the user's ideas are recorded immediately. + +2. **Action before asking** + If you can derive the answer from repo code, docs, configs, conventions, or quick research — do that first. + +3. **One question per message** + Never overwhelm the user with a list of questions. Ask one, update PRD, repeat. + +4. **Prefer concrete options** + For preference/decision questions, present 2–3 feasible, specific approaches with trade-offs. + +5. **Research-first for technical choices** + If the decision depends on industry conventions / similar tools / established patterns, do research first, then propose options. + +6. **Diverge → Converge** + After initial understanding, proactively consider future evolution, related scenarios, and failure/edge cases — then converge to an MVP with explicit out-of-scope. + +7. **No meta questions** + Do not ask "should I search?" or "can you paste the code so I can continue?" + If you need information: search/inspect. If blocked: ask the minimal blocking question. + +--- + +## Step 0: Ensure Task Exists (ALWAYS) + +Before any Q&A, ensure a task exists. If none exists, create one immediately. + +* Use a **temporary working title** derived from the user's message. +* It's OK if the title is imperfect — refine later in PRD. + +```bash +TASK_DIR=$(python3 ./.trellis/scripts/task.py create "brainstorm: " --slug ) +``` + +Use a slug without a date prefix. `task.py create` adds the `MM-DD-` +directory prefix automatically. + +Create/seed `prd.md` immediately with what you know: + +```markdown +# brainstorm: + +## Goal + + + +## What I already know + +* +* + +## Assumptions (temporary) + +* + +## Open Questions + +* + +## Requirements (evolving) + +* + +## Acceptance Criteria (evolving) + +* [ ] + +## Definition of Done (team quality bar) + +* Tests added/updated (unit/integration where appropriate) +* Lint / typecheck / CI green +* Docs/notes updated if behavior changes +* Rollout/rollback considered if risky + +## Out of Scope (explicit) + +* + +## Technical Notes + +* +* +``` + +--- + +## Step 1: Auto-Context (DO THIS BEFORE ASKING QUESTIONS) + +Before asking questions like "what does the code look like?", gather context yourself: + +### Repo inspection checklist + +* Identify likely modules/files impacted +* Locate existing patterns (similar features, conventions, error handling style) +* Check configs, scripts, existing command definitions +* Note any constraints (runtime, dependency policy, build tooling) + +### Documentation checklist + +* Look for existing PRDs/specs/templates +* Look for command usage examples, README, ADRs if any + +Write findings into PRD: + +* Add to `What I already know` +* Add constraints/links to `Technical Notes` + +--- + +## Step 2: Classify Complexity (still useful, not gating task creation) + +| Complexity | Criteria | Action | +| ------------ | ------------------------------------------------------ | ------------------------------------------- | +| **Trivial** | Single-line fix, typo, obvious change | Skip brainstorm, implement directly | +| **Simple** | Clear goal, 1–2 files, scope well-defined | Ask 1 confirm question, then implement | +| **Moderate** | Multiple files, some ambiguity | Light brainstorm (2–3 high-value questions) | +| **Complex** | Vague goal, architectural choices, multiple approaches | Full brainstorm | + +> Note: Task already exists from Step 0. Classification only affects depth of brainstorming. + +--- + +## Step 3: Question Gate (Ask ONLY high-value questions) + +Before asking ANY question, run the following gate: + +### Gate A — Can I derive this without the user? + +If answer is available via: + +* repo inspection (code/config) +* docs/specs/conventions +* quick market/OSS research + +→ **Do not ask.** Fetch it, summarize, update PRD. + +### Gate B — Is this a meta/lazy question? + +Examples: + +* "Should I search?" +* "Can you paste the code so I can proceed?" +* "What does the code look like?" (when repo is available) + +→ **Do not ask.** Take action. + +### Gate C — What type of question is it? + +* **Blocking**: cannot proceed without user input +* **Preference**: multiple valid choices, depends on product/UX/risk preference +* **Derivable**: should be answered by inspection/research + +→ Only ask **Blocking** or **Preference**. + +--- + +## Step 4: Research-first Mode (Mandatory for technical choices) + +### Trigger conditions (any → research-first) + +* The task involves selecting an approach, library, protocol, framework, template system, plugin mechanism, or CLI UX convention +* The user asks for "best practice", "how others do it", "recommendation" +* The user can't reasonably enumerate options + +### Delegate to `trellis-research` sub-agent (don't research inline) + +For each research topic, **spawn a `trellis-research` sub-agent via the Task tool** — don't do WebFetch / WebSearch / `gh api` inline in the main conversation. + +Why: +- The sub-agent has its own context window → doesn't pollute brainstorm context with raw tool output +- It persists findings to `{TASK_DIR}/research/.md` (the contract — see `workflow.md` Phase 1.2) +- It returns only `{file path, one-line summary}` to the main agent +- Independent topics can be **parallelized** — spawn multiple sub-agents in one tool call + +> **Codex exception**: on Codex CLI, do NOT dispatch `trellis-research` for research-first mode — do the research inline (WebFetch / WebSearch in the main session) and write findings to `{TASK_DIR}/research/.md` yourself. Reason: Codex `spawn_agent` runs sub-agents with `fork_turns="none"` (isolated context, no parent session inheritance), so the research sub-agent cannot resolve the active task path via `task.py current` and silently aborts without producing files. Inline research on Codex avoids this failure mode. The 3+ inline research calls limit (B rule in `workflow.md`) is relaxed for Codex specifically. + +Agent type: `trellis-research` +Task description template: "Research ; persist findings to `{TASK_DIR}/research/.md`." + +❌ Bad (what you must NOT do): +``` +Main agent: WebFetch(url-A) → WebFetch(url-B) → Bash(gh api ...) + → WebSearch(q1) → WebSearch(q2) → ... (10+ inline calls) + → Write(research/topic.md) +``` +→ Pollutes main context with raw HTML/JSON, burns tokens. + +✅ Good: +``` +Main agent: Task(subagent_type="trellis-research", + prompt="Research topic A; persist to research/topic-a.md") + + Task(subagent_type="trellis-research", + prompt="Research topic B; persist to research/topic-b.md") + + Task(subagent_type="trellis-research", + prompt="Research topic C; persist to research/topic-c.md") +→ Reads research/topic-{a,b,c}.md after they finish. +``` + +### Research steps (to pass into each sub-agent prompt) + +Each `trellis-research` sub-agent should: + +1. Identify 2–4 comparable tools/patterns for its topic +2. Summarize common conventions and why they exist +3. Map conventions onto our repo constraints +4. Write findings to `{TASK_DIR}/research/.md` + +Main agent then reads the persisted files and produces **2–3 feasible approaches** in PRD. + +### Research output format (PRD) + +The PRD itself should only reference the persisted research files, not duplicate their content. Add a `## Research References` section pointing at `research/*.md`. + +Optionally, add a convergence section with feasible approaches derived from the research: + +```markdown +## Research References + +* [`research/.md`](research/.md) — +* [`research/.md`](research/.md) — + +## Research Notes + +### What similar tools do + +* ... +* ... + +### Constraints from our repo/project + +* ... + +### Feasible approaches here + +**Approach A: ** (Recommended) + +* How it works: +* Pros: +* Cons: + +**Approach B: ** + +* How it works: +* Pros: +* Cons: + +**Approach C: ** (optional) + +* ... +``` + +Then ask **one** preference question: + +* "Which approach do you prefer: A / B / C (or other)?" + +--- + +## Step 5: Expansion Sweep (DIVERGE) — Required after initial understanding + +After you can summarize the goal, proactively broaden thinking before converging. + +### Expansion categories (keep to 1–2 bullets each) + +1. **Future evolution** + + * What might this feature become in 1–3 months? + * What extension points are worth preserving now? + +2. **Related scenarios** + + * What adjacent commands/flows should remain consistent with this? + * Are there parity expectations (create vs update, import vs export, etc.)? + +3. **Failure & edge cases** + + * Conflicts, offline/network failure, retries, idempotency, compatibility, rollback + * Input validation, security boundaries, permission checks + +### Expansion message template (to user) + +```markdown +I understand you want to implement: . + +Before diving into design, let me quickly diverge to consider three categories (to avoid rework later): + +1. Future evolution: <1–2 bullets> +2. Related scenarios: <1–2 bullets> +3. Failure/edge cases: <1–2 bullets> + +For this MVP, which would you like to include (or none)? + +1. Current requirement only (minimal viable) +2. Add (reserve for future extension) +3. Add (improve robustness/consistency) +4. Other: describe your preference +``` + +Then update PRD: + +* What's in MVP → `Requirements` +* What's excluded → `Out of Scope` + +--- + +## Step 6: Q&A Loop (CONVERGE) + +### Rules + +* One question per message +* Prefer multiple-choice when possible +* After each user answer: + + * Update PRD immediately + * Move answered items from `Open Questions` → `Requirements` + * Update `Acceptance Criteria` with testable checkboxes + * Clarify `Out of Scope` + +### Question priority (recommended) + +1. **MVP scope boundary** (what is included/excluded) +2. **Preference decisions** (after presenting concrete options) +3. **Failure/edge behavior** (only for MVP-critical paths) +4. **Success metrics & Acceptance Criteria** (what proves it works) + +### Preferred question format (multiple choice) + +```markdown +For , which approach do you prefer? + +1. **Option A** — +2. **Option B** — +3. **Option C** — +4. **Other** — describe your preference +``` + +--- + +## Step 7: Propose Approaches + Record Decisions (Complex tasks) + +After requirements are clear enough, propose 2–3 approaches (if not already done via research-first): + +```markdown +Based on current information, here are 2–3 feasible approaches: + +**Approach A: ** (Recommended) + +* How: +* Pros: +* Cons: + +**Approach B: ** + +* How: +* Pros: +* Cons: + +Which direction do you prefer? +``` + +Record the outcome in PRD as an ADR-lite section: + +```markdown +## Decision (ADR-lite) + +**Context**: Why this decision was needed +**Decision**: Which approach was chosen +**Consequences**: Trade-offs, risks, potential future improvements +``` + +--- + +## Step 8: Final Confirmation + Implementation Plan + +When open questions are resolved, confirm complete requirements with a structured summary: + +### Final confirmation format + +```markdown +Here's my understanding of the complete requirements: + +**Goal**: + +**Requirements**: + +* ... +* ... + +**Acceptance Criteria**: + +* [ ] ... +* [ ] ... + +**Definition of Done**: + +* ... + +**Out of Scope**: + +* ... + +**Technical Approach**: + + +**Implementation Plan (small PRs)**: + +* PR1: +* PR2: +* PR3: + +Does this look correct? If yes, I'll proceed with implementation. +``` + +### Subtask Decomposition (Complex Tasks) + +For complex tasks with multiple independent work items, create subtasks: + +```bash +# Create child tasks +CHILD1=$(python3 ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR") +CHILD2=$(python3 ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR") + +# Or link existing tasks +python3 ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR" +``` + +--- + +## PRD Target Structure (final) + +`prd.md` should converge to: + +```markdown +# + +## Goal + + + +## Requirements + +* ... + +## Acceptance Criteria + +* [ ] ... + +## Definition of Done + +* ... + +## Technical Approach + + + +## Decision (ADR-lite) + +Context / Decision / Consequences + +## Out of Scope + +* ... + +## Technical Notes + + +``` + +--- + +## Anti-Patterns (Hard Avoid) + +* Asking user for code/context that can be derived from repo +* Asking user to choose an approach before presenting concrete options +* Meta questions about whether to research +* Staying narrowly on the initial request without considering evolution/edges +* Letting brainstorming drift without updating PRD + +--- + +## Integration with Start Workflow + +After brainstorm completes (Step 8 confirmation approved), the flow continues to the Task Workflow's **Phase 2: Prepare for Implementation**: + +```text +Brainstorm + Step 0: Create task directory + seed PRD + Step 1–7: Discover requirements, research, converge + Step 8: Final confirmation → user approves + ↓ +Task Workflow Phase 2 (Prepare for Implementation) + Code-Spec Depth Check (if applicable) + → Research codebase (based on confirmed PRD) + → Configure code-spec context (jsonl files) + → Activate task + ↓ +Task Workflow Phase 3 (Execute) + Implement → Check → Complete +``` + +The task directory and PRD already exist from brainstorm, so Phase 1 of the Task Workflow is skipped entirely. + +--- + +## Related Commands + +| Command | When to Use | +|---------|-------------| +| `/trellis:start` | Entry point that triggers brainstorm | +| `/trellis:finish-work` | After implementation is complete | +| `/trellis:update-spec` | If new patterns emerge during work | diff --git a/.omp/skills/trellis-break-loop/SKILL.md b/.omp/skills/trellis-break-loop/SKILL.md new file mode 100644 index 000000000..ef2b50cd8 --- /dev/null +++ b/.omp/skills/trellis-break-loop/SKILL.md @@ -0,0 +1,130 @@ +--- +name: trellis-break-loop +description: "Deep bug analysis to break the fix-forget-repeat cycle. Analyzes root cause category, why fixes failed, prevention mechanisms, and captures knowledge into specs. Use after fixing a bug to prevent the same class of bugs." +--- + +# Break the Loop - Deep Bug Analysis + +When debug is complete, use this for deep analysis to break the "fix bug -> forget -> repeat" cycle. + +--- + +## Analysis Framework + +Analyze the bug you just fixed from these 5 dimensions: + +### 1. Root Cause Category + +Which category does this bug belong to? + +| Category | Characteristics | Example | +|----------|-----------------|---------| +| **A. Missing Spec** | No documentation on how to do it | New feature without checklist | +| **B. Cross-Layer Contract** | Interface between layers unclear | API returns different format than expected | +| **C. Change Propagation Failure** | Changed one place, missed others | Changed function signature, missed call sites | +| **D. Test Coverage Gap** | Unit test passes, integration fails | Works alone, breaks when combined | +| **E. Implicit Assumption** | Code relies on undocumented assumption | Timestamp seconds vs milliseconds | + +### 2. Why Fixes Failed (if applicable) + +If you tried multiple fixes before succeeding, analyze each failure: + +- **Surface Fix**: Fixed symptom, not root cause +- **Incomplete Scope**: Found root cause, didn't cover all cases +- **Tool Limitation**: grep missed it, type check wasn't strict +- **Mental Model**: Kept looking in same layer, didn't think cross-layer + +### 3. Prevention Mechanisms + +What mechanisms would prevent this from happening again? + +| Type | Description | Example | +|------|-------------|---------| +| **Documentation** | Write it down so people know | Update thinking guide | +| **Architecture** | Make the error impossible structurally | Type-safe wrappers | +| **Compile-time** | Strict type checking, no escape hatches | Signature change causes compile error | +| **Runtime** | Monitoring, alerts, scans | Detect orphan entities | +| **Test Coverage** | E2E tests, integration tests | Verify full flow | +| **Code Review** | Checklist, PR template | "Did you check X?" | + +### 4. Systematic Expansion + +What broader problems does this bug reveal? + +- **Similar Issues**: Where else might this problem exist? +- **Design Flaw**: Is there a fundamental architecture issue? +- **Process Flaw**: Is there a development process improvement? +- **Knowledge Gap**: Is the team missing some understanding? + +### 5. Knowledge Capture + +Solidify insights into the system: + +- [ ] Update `.trellis/spec/guides/` thinking guides +- [ ] Update relevant `.trellis/spec/` docs +- [ ] Create issue record (if applicable) +- [ ] Create feature ticket for root fix +- [ ] Update check guidelines if needed + +--- + +## Output Format + +Please output analysis in this format: + +```markdown +## Bug Analysis: [Short Description] + +### 1. Root Cause Category +- **Category**: [A/B/C/D/E] - [Category Name] +- **Specific Cause**: [Detailed description] + +### 2. Why Fixes Failed (if applicable) +1. [First attempt]: [Why it failed] +2. [Second attempt]: [Why it failed] +... + +### 3. Prevention Mechanisms +| Priority | Mechanism | Specific Action | Status | +|----------|-----------|-----------------|--------| +| P0 | ... | ... | TODO/DONE | + +### 4. Systematic Expansion +- **Similar Issues**: [List places with similar problems] +- **Design Improvement**: [Architecture-level suggestions] +- **Process Improvement**: [Development process suggestions] + +### 5. Knowledge Capture +- [ ] [Documents to update / tickets to create] +``` + +--- + +## Core Philosophy + +> **The value of debugging is not in fixing the bug, but in making this class of bugs never happen again.** + +Three levels of insight: +1. **Tactical**: How to fix THIS bug +2. **Strategic**: How to prevent THIS CLASS of bugs +3. **Philosophical**: How to expand thinking patterns + +30 minutes of analysis saves 30 hours of future debugging. + +--- + +## After Analysis: Immediate Actions + +**IMPORTANT**: After completing the analysis above, you MUST immediately: + +1. **Update spec/guides** - Don't just list TODOs, actually update the relevant files: + - If it's a cross-platform issue → update `cross-platform-thinking-guide.md` + - If it's a cross-layer issue → update `cross-layer-thinking-guide.md` + - If it's a code reuse issue → update `code-reuse-thinking-guide.md` + - If it's domain-specific → update `backend/*.md` or `frontend/*.md` + +2. **Sync templates** - After updating `.trellis/spec/`, sync to `src/templates/markdown/spec/` + +3. **Commit the spec updates** - This is the primary output, not just the analysis text + +> **The analysis is worthless if it stays in chat. The value is in the updated specs.** diff --git a/.omp/skills/trellis-check/SKILL.md b/.omp/skills/trellis-check/SKILL.md new file mode 100644 index 000000000..16b3dc49c --- /dev/null +++ b/.omp/skills/trellis-check/SKILL.md @@ -0,0 +1,92 @@ +--- +name: trellis-check +description: "Comprehensive quality verification: spec compliance, lint, type-check, tests, cross-layer data flow, code reuse, and consistency checks. Use when code is written and needs quality verification, before committing changes, or to catch context drift during long sessions." +--- + +# Code Quality Check + +Comprehensive quality verification for recently written code. Combines spec compliance, cross-layer safety, and pre-commit checks. + +--- + +## Step 1: Identify What Changed + +```bash +git diff --name-only HEAD +git status +``` + +## Step 2: Read Applicable Specs + +```bash +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///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/.omp/skills/trellis-meta/SKILL.md b/.omp/skills/trellis-meta/SKILL.md new file mode 100644 index 000000000..590bfac3f --- /dev/null +++ b/.omp/skills/trellis-meta/SKILL.md @@ -0,0 +1,73 @@ +--- +name: trellis-meta +description: "Understand and customize the local Trellis architecture inside a user project. Use when modifying .trellis plus platform hooks, settings, agents, skills, commands, prompts, or workflows generated by trellis init." +--- + +# Trellis Meta + +This skill is for local Trellis users who have already run `trellis init` in a project. After reading it, an AI should understand the Trellis architecture, operating model, and customization entry points inside that user project, then modify the generated `.trellis/` and platform directory files according to the user's request. + +The default operating scope is local files in the user project: + +- `.trellis/`: workflow, config, tasks, spec, workspace, scripts, and runtime state. +- Platform directories: `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, `.kiro/`, `.gemini/`, `.qoder/`, `.codebuddy/`, `.github/`, `.factory/`, `.pi/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories. +- Shared skill layer: `.agents/skills/`. + +Do not assume the user has the Trellis source repository. Do not default to modifying the global npm install directory or `node_modules`. + +## How To Use + +1. Read `references/local-architecture/overview.md` first to establish the local Trellis system model. +2. If the request involves a specific AI tool, read `references/platform-files/platform-map.md` and the relevant platform file notes. +3. If the user wants to change behavior, read `references/customize-local/overview.md`, then open the specific customization topic. +4. Before editing, read the actual files in the user project and treat local content as authoritative. + +## References + +### Local Architecture + +- `references/local-architecture/overview.md`: The three-layer local Trellis architecture and customization principles. +- `references/local-architecture/generated-files.md`: Files generated by `trellis init` and their customization boundaries. +- `references/local-architecture/workflow.md`: Phases, routing, and workflow-state blocks in `.trellis/workflow.md`. +- `references/local-architecture/task-system.md`: Task directories, active tasks, JSONL context, and task runtime. +- `references/local-architecture/spec-system.md`: How `.trellis/spec/` is organized and injected. +- `references/local-architecture/workspace-memory.md`: `.trellis/workspace/`, journals, and cross-session memory. +- `references/local-architecture/context-injection.md`: Hooks, sub-agent preludes, and context injection paths. + +### Platform Files + +- `references/platform-files/overview.md`: How shared `.trellis/` files relate to platform directories. +- `references/platform-files/platform-map.md`: Platform directories and paths for skills, agents, hooks, and extensions. +- `references/platform-files/hooks-and-settings.md`: How settings/config files, hooks, plugins, and extensions connect to Trellis. +- `references/platform-files/agents.md`: Local file responsibilities for `trellis-research`, `trellis-implement`, and `trellis-check`. +- `references/platform-files/skills-and-commands.md`: Differences between skills, commands, prompts, and workflows, plus how to change them. + +### Local Customization + +- `references/customize-local/overview.md`: Choose the right local customization entry point for the user's request. +- `references/customize-local/change-workflow.md`: Change phases, routing, next actions, and workflow-state. +- `references/customize-local/change-task-lifecycle.md`: Change task creation, status, archive behavior, and hooks. +- `references/customize-local/change-context-loading.md`: Change how tasks, specs, journals, and hook context are loaded. +- `references/customize-local/change-hooks.md`: Change platform hooks, settings, and shell session bridges. +- `references/customize-local/change-agents.md`: Change research, implement, and check agent behavior. +- `references/customize-local/change-skills-or-commands.md`: Add or modify local skills, commands, prompts, and workflows. +- `references/customize-local/change-spec-structure.md`: Adjust the project spec structure under `.trellis/spec/`. +- `references/customize-local/add-project-local-conventions.md`: Put team rules into project-local specs or local skills. + +## Current Rules + +- `.trellis/workflow.md` is the local workflow source of truth. +- `.trellis/config.yaml` is the project-level Trellis configuration and task hook configuration entry point. +- `.trellis/spec/` stores the user's project-specific coding conventions and design constraints. +- `.trellis/tasks/` stores task PRDs, technical notes, research files, and JSONL context. +- `.trellis/workspace/` stores developer journals and cross-session memory. +- Platform settings/config files decide which hooks, agents, skills, commands, prompts, and workflows actually run. +- `.trellis/.template-hashes.json` and `.trellis/.runtime/` are management/runtime state files. Confirm necessity before editing them. + +## Do Not + +- Do not treat Trellis upstream source code as the default target for local customization. +- Do not modify the global npm install directory or `node_modules/@mindfoldhq/trellis` to implement project needs. +- Do not overwrite user-modified local files with default templates. +- Do not put team-private project rules into the public `trellis-meta`; put project rules in `.trellis/spec/` or a project-local skill. +- Do not describe removed historical mechanisms as current Trellis behavior. diff --git a/.omp/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.omp/skills/trellis-meta/references/customize-local/add-project-local-conventions.md new file mode 100644 index 000000000..608aaa6f6 --- /dev/null +++ b/.omp/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//` | +| Cross-layer thinking methods | `.trellis/spec/guides/` | +| AI capability for a project-specific flow | Platform-local skill | +| One-off task material | `.trellis/tasks//` | +| Session summary | `.trellis/workspace//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 implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" +python3 ./.trellis/scripts/task.py add-context 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/.omp/skills/trellis-meta/references/customize-local/change-agents.md b/.omp/skills/trellis-meta/references/customize-local/change-agents.md new file mode 100644 index 000000000..9b6353107 --- /dev/null +++ b/.omp/skills/trellis-meta/references/customize-local/change-agents.md @@ -0,0 +1,54 @@ +# Change Local Agents + +When the user wants to change `trellis-research`, `trellis-implement`, or `trellis-check` behavior, edit platform agent files in the user project. + +## Read These Files First + +1. Target platform agent directory +2. `.trellis/workflow.md` Phase 2 / research routing +3. Current task `prd.md` +4. Current task `implement.jsonl` / `check.jsonl` +5. Relevant hook or agent prelude + +## Common Paths + +| Platform | Path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | + +Use the actual paths in the user project as authoritative. + +## Common Needs + +| Need | Which agent to edit | +| --- | --- | +| Research must write files, not only reply in chat | `trellis-research` | +| Certain local specs must be read before implementation | `trellis-implement` + `implement.jsonl` configuration rules | +| Specific commands must run during checking | `trellis-check` | +| Agent must not modify certain directories | The corresponding agent's write boundary instructions | +| Agent output format must be fixed | The corresponding agent's final/reporting instructions | + +## Modification Principles + +1. **Preserve role boundaries**: research investigates and persists; implement writes implementation; check reviews and fixes. +2. **Do not hard-code project specs into agents**: long-term specs belong in `.trellis/spec/`; agents are responsible for reading them. +3. **Make read order explicit**: active task -> PRD -> info -> JSONL -> spec/research. +4. **Make write boundaries explicit**: which directories may be written and which may not. +5. **Synchronize across platforms**: when the user configured multiple platforms, decide whether to change only the current platform or all platform agents. + +## Agent Pull Platforms + +If an agent file contains a prelude for "read task/context after startup," do not remove those steps when editing. Otherwise the agent will work only from chat context and bypass Trellis's core mechanism. + +## Hook Push Platforms + +If context is injected by a hook, the agent file should still retain responsibility boundaries. Do not remove PRD/spec requirements from the agent just because a hook injects context. diff --git a/.omp/skills/trellis-meta/references/customize-local/change-context-loading.md b/.omp/skills/trellis-meta/references/customize-local/change-context-loading.md new file mode 100644 index 000000000..556b4e382 --- /dev/null +++ b/.omp/skills/trellis-meta/references/customize-local/change-context-loading.md @@ -0,0 +1,81 @@ +# Change Local Context Loading + +Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." + +## Read These Files First + +1. `.trellis/workflow.md` +2. `.trellis/scripts/get_context.py` +3. `.trellis/scripts/common/session_context.py` +4. `.trellis/scripts/common/task_context.py` +5. `.trellis/scripts/common/active_task.py` +6. Current platform hooks or agent files +7. The current task's `implement.jsonl` / `check.jsonl` + +## Context Sources + +| Source | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow and next-action hints. | +| `.trellis/tasks//prd.md` | Current task requirements. | +| `.trellis/tasks//implement.jsonl` | Spec/research to read before implementation. | +| `.trellis/tasks//check.jsonl` | Spec/research to read during checking. | +| `.trellis/spec/` | Project specs. | +| `.trellis/workspace/` | Session records. | +| git status | Current working tree changes. | + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | +| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | +| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | +| Active task is lost | `active_task.py` and platform session identity propagation. | +| Change JSONL validation rules | `task_context.py`. | + +## JSONL Rules + +`implement.jsonl` / `check.jsonl` are the key context loading interface: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} +``` + +Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. + +## Change Session Context + +If the user wants every new session to see more project state, edit: + +- `.trellis/scripts/common/session_context.py` +- the corresponding platform `session-start` hook + +Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. + +## Change Sub-Agent Context + +First determine which mode the platform uses: + +- hook push: edit the `inject-subagent-context` hook. +- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. + +In both modes, make sure the agent ultimately reads: + +1. active task +2. `prd.md` +3. `info.md` if present +4. the corresponding JSONL +5. spec/research referenced by the JSONL + +## Troubleshooting Order + +```bash +python3 ./.trellis/scripts/task.py current --source +python3 ./.trellis/scripts/task.py list-context +python3 ./.trellis/scripts/task.py validate +python3 ./.trellis/scripts/get_context.py --mode packages +``` + +Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.omp/skills/trellis-meta/references/customize-local/change-hooks.md b/.omp/skills/trellis-meta/references/customize-local/change-hooks.md new file mode 100644 index 000000000..5c1ed7a40 --- /dev/null +++ b/.omp/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` +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 +``` + +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/.omp/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.omp/skills/trellis-meta/references/customize-local/change-skills-or-commands.md new file mode 100644 index 000000000..84590a118 --- /dev/null +++ b/.omp/skills/trellis-meta/references/customize-local/change-skills-or-commands.md @@ -0,0 +1,78 @@ +# Change Local Skills, Commands, Prompts, And Workflows + +When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Target platform skill/command/prompt/workflow directory +3. Related agent or hook files +4. Whether project rules already exist in `.trellis/spec/` + +## Which Entry Type To Choose + +| Goal | Recommendation | +| --- | --- | +| AI should automatically know a capability | Add or modify a skill. | +| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | +| Team project conventions | Prefer `.trellis/spec/` or a project-local skill. | +| Change Trellis flow semantics | Synchronize `.trellis/workflow.md`. | + +## Modify A Skill + +A skill is usually: + +```text +/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. + +The frontmatter description should specify when to use the skill. Example: + +```yaml +description: "Use when customizing this project's deployment workflow and release checklist." +``` + +Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. + +## Modify A Command/Prompt/Workflow + +Explicit entry points should state: + +- How the user triggers it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. + +## Common Paths + +| Platform | Entry directories | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| GitHub Copilot | `.github/skills/`, `.github/prompts/` | +| Kilo / Antigravity / Windsurf | workflows + skills | + +## Add A Project-Local Skill + +If the user wants to document team-private customizations, create a project-local skill, for example: + +```text +.claude/skills/project-trellis-local/ +└── SKILL.md +``` + +For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer. + +## Notes + +- Do not mix every platform's syntax into one file. +- Do not change only one platform entry point while claiming all platforms are supported. +- Do not hide long-term engineering conventions inside a command; write them to `.trellis/spec/`. diff --git a/.omp/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.omp/skills/trellis-meta/references/customize-local/change-spec-structure.md new file mode 100644 index 000000000..358de513a --- /dev/null +++ b/.omp/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` Phase 1.3 and Phase 3.3 +4. Current task `implement.jsonl` / `check.jsonl` + +## Common Needs + +| Need | Edit location | +| --- | --- | +| Add backend/frontend/docs/test spec layer | `.trellis/spec//` or `.trellis/spec///` | +| 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 implement ".trellis/spec/webapp/security/index.md" "Security conventions" +python3 ./.trellis/scripts/task.py add-context 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/.omp/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.omp/skills/trellis-meta/references/customize-local/change-task-lifecycle.md new file mode 100644 index 000000000..a7a340fba --- /dev/null +++ b/.omp/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.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/.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 ` 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/.omp/skills/trellis-meta/references/customize-local/change-workflow.md b/.omp/skills/trellis-meta/references/customize-local/change-workflow.md new file mode 100644 index 000000000..4231845ad --- /dev/null +++ b/.omp/skills/trellis-meta/references/customize-local/change-workflow.md @@ -0,0 +1,64 @@ +# Change Local Workflow + +When the user wants to change Trellis phases, next-action hints, whether to create tasks, whether to use sub-agents, or when to check/wrap up, edit `.trellis/workflow.md` first. + +## Read These Files First + +1. `.trellis/workflow.md` +2. Entry files for the current platform, such as skills/commands/prompts/workflows +3. The current task's `task.json` and `prd.md` + +## Common Needs And Edit Points + +| Need | Edit point | +| --- | --- | +| Change phase names or phase order | `Phase Index` and the corresponding Phase sections. | +| Change whether to create a task when there is no task | `[workflow-state:no_task]` state block. | +| Change the next step during planning | Phase 1 and `[workflow-state:planning]`. | +| Change whether an agent is required during in_progress | Phase 2 and `[workflow-state:in_progress]`. | +| Change wrap-up after completion | Phase 3 and `[workflow-state:completed]`. | +| Change which skill a user intent triggers | `Skill Routing` table. | + +## Modification Steps + +1. Find the relevant section in `.trellis/workflow.md`. +2. When changing rules, keep explicit trigger conditions and next actions. +3. If adding or renaming a skill/agent, synchronize the corresponding files in platform directories. +4. Workflow-state changes only need an edit to the `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook is parser-only — it reads whatever you put in the block. Keep the opening and closing tags' STATUS strings identical (`[workflow-state:foo]…[/workflow-state:foo]`); mismatched STATUS pairs are silently dropped. +5. Make the AI reread `.trellis/workflow.md`; do not keep using rules from the old conversation. + +## Example: Relax Task Creation Requirements + +To change when task creation can be skipped, usually edit `[workflow-state:no_task]`: + +```md +[workflow-state:no_task] +Task is not required when the answer is a one-reply explanation, no files are changed, and no research is needed. +[/workflow-state:no_task] +``` + +If the formal Phase 1 flow also needs to change, synchronize the Phase 1 section. + +## Example: One Platform Does Not Use Sub-Agents + +If the user wants only one platform to avoid sub-agents, first confirm whether that platform has a separate group in the workflow. Then change Phase 2 routing for that platform group instead of deleting all `trellis-implement` / `trellis-check` instructions across platforms. + +## `/trellis:continue` Route Table + +`/trellis:continue` resumes a task by deciding which phase step to load next. The decision combines `task.json.status` with the presence of artifacts inside the task directory. The mapping is fixed in the command itself; forks that add custom statuses must extend both the workflow.md tag block and this table. + +| `status` | Artifact state | Resume at | +| --- | --- | --- | +| `planning` | `prd.md` missing | Phase 1.1 (load `trellis-brainstorm`) | +| `planning` | `prd.md` exists, `implement.jsonl` only has the seed `_example` row | Phase 1.3 (curate JSONL context) | +| `planning` | `prd.md` exists, `implement.jsonl` curated | Phase 1.4 (run `task.py start`) | +| `in_progress` | no implementation in conversation history | Phase 2.1 (`trellis-implement`) | +| `in_progress` | implementation done, no `trellis-check` run | Phase 2.2 (`trellis-check`) | +| `in_progress` | check passed | Phase 3.1 (verify quality + spec update) | +| `completed` | task is still in active tree | Phase 3.5 (run `/trellis:finish-work` to archive) | + +When you add a custom status (e.g. `in-review`), add a `[workflow-state:in-review]` block in `.trellis/workflow.md` for the per-turn breadcrumb AND extend this route table — usually by editing the `/trellis:continue` command file (`.{platform}/commands/trellis/continue.md` or equivalent) to add a row that decides where to resume from. Without the route entry, `/trellis:continue` will fall through to a default branch and the user will not land on the step you intended. + +## Notes + +`.trellis/workflow.md` is the local project workflow, not an immutable template. The user can adapt it to team habits. After editing it, platform entry files may still contain old descriptions, so inspect them too. diff --git a/.omp/skills/trellis-meta/references/customize-local/overview.md b/.omp/skills/trellis-meta/references/customize-local/overview.md new file mode 100644 index 000000000..b53b090f0 --- /dev/null +++ b/.omp/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/`. +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//` | +| Project specs | `.trellis/spec/` | +| Runtime scripts | `.trellis/scripts/` | +| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, and similar directories | +| Shared skill | `.agents/skills/` | + +## Things Not To Do By Default + +- Do not edit the global npm install directory. +- Do not edit `node_modules/@mindfoldhq/trellis`. +- Do not assume the user has the Trellis GitHub repository. +- Do not overwrite local files already modified by the user with default templates. +- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. + +## When To Inspect Upstream Source + +Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: + +- "I want to open a PR to Trellis" +- "I want to change npm package publish contents" +- "I want to fork Trellis" +- "I want to modify the generation logic for `trellis init/update`" + +Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.omp/skills/trellis-meta/references/local-architecture/context-injection.md b/.omp/skills/trellis-meta/references/local-architecture/context-injection.md new file mode 100644 index 000000000..fae6fa581 --- /dev/null +++ b/.omp/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//prd.md`, `info.md`, `research/` | Current task requirements, design, and research. | +| platform context | Platform hooks/settings/agents | Lets different AI tools read the files above through their own mechanisms. | + +## session-start + +Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: + +- workflow summary. +- current task status. +- active tasks. +- spec index paths. +- developer identity and git status. + +If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. + +## workflow-state + +workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. + +If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. + +## sub-agent context + +Implement and check agents need task context. Trellis has two loading modes: + +1. **hook push**: a platform hook injects `prd.md` and the files referenced by `implement.jsonl` / `check.jsonl` before the agent starts. +2. **agent pull**: the agent definition instructs the agent to read the active task, PRD, and JSONL context after startup. + +In both modes, JSONL files in the task directory are the key interface. + +## JSONL Reading Rules + +`implement.jsonl` and `check.jsonl` contain one JSON object per line: + +```jsonl +{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} +``` + +Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. + +## Active Task And Context Key + +Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. + +If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Change session-start injected content | The platform's `session-start` hook or plugin file. | +| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | +| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | +| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | +| Change active task resolution | `.trellis/scripts/common/active_task.py`. | + +When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct PRD/spec/research. diff --git a/.omp/skills/trellis-meta/references/local-architecture/generated-files.md b/.omp/skills/trellis-meta/references/local-architecture/generated-files.md new file mode 100644 index 000000000..66f832d7d --- /dev/null +++ b/.omp/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` | Tell the platform when to run hooks or plugins. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define agents such as `trellis-research`, `trellis-implement`, and `trellis-check`. | +| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Skills that auto-trigger or can be read by AI. | +| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Explicit user-invoked command or workflow entry points. | + +When modifying a platform directory, also confirm whether `.trellis/workflow.md` still describes the same flow. + +## Meaning Of Template Hashes + +`.trellis/.template-hashes.json` records the content hash from the last time Trellis wrote a template file. `trellis update` uses it to distinguish three cases: + +| Case | Update behavior | +| --- | --- | +| File was not modified by the user | It can be updated automatically. | +| File was modified by the user | Prompt the user to overwrite, keep, or generate `.new`. | +| File is no longer a current template | It may be deleted, renamed, or preserved according to migration rules. | + +When an AI customizes local Trellis files, it does not need to maintain hashes manually. It is normal for Trellis update to recognize the result as "modified by the user." + +## Local Customization Boundaries + +Editable by default: + +- `.trellis/workflow.md` +- `.trellis/config.yaml` +- `.trellis/spec/**` +- `.trellis/scripts/**` +- Platform hooks, settings, agents, skills, commands, prompts, and workflows + +Do not edit by default: + +- Global npm install directory +- `node_modules/@mindfoldhq/trellis` +- Trellis GitHub repository source code +- Concrete state files under `.trellis/.runtime/**` +- Hash contents inside `.trellis/.template-hashes.json` + +Switch to the Trellis CLI source-code perspective only when the user explicitly wants to contribute upstream. diff --git a/.omp/skills/trellis-meta/references/local-architecture/overview.md b/.omp/skills/trellis-meta/references/local-architecture/overview.md new file mode 100644 index 000000000..99c7f7368 --- /dev/null +++ b/.omp/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/`, `.windsurf/`, and similar directories. +- Shared skill layer: `.agents/skills/`. + +Do not default to guiding the user to fork the Trellis CLI repository. Treat upstream source code as the operating target only when the user explicitly says they want to change Trellis upstream source, publish an npm package, or contribute a PR. + +## Local System Model + +Trellis provides three layers inside a user project: + +1. **Workflow layer**: `.trellis/workflow.md` defines phases, routing, next actions, and prompt blocks. +2. **Persistence layer**: `.trellis/tasks/`, `.trellis/spec/`, and `.trellis/workspace/` store tasks, specs, and session memory. +3. **Platform integration layer**: hooks, settings, agents, skills, commands, prompts, and workflows in platform directories connect the Trellis workflow to different AI tools. + +All three layers live inside the user project, so an AI can read and modify them directly. + +## Core Paths + +| Path | Purpose | +| --- | --- | +| `.trellis/workflow.md` | Workflow phases, skill routing, and workflow-state prompt blocks. | +| `.trellis/config.yaml` | Project configuration, task lifecycle hooks, monorepo package configuration, and journal configuration. | +| `.trellis/spec/` | The user's project-specific coding conventions and thinking guides. | +| `.trellis/tasks/` | Each task's PRD, technical notes, research files, and JSONL context. | +| `.trellis/workspace/` | Per-developer journals and cross-session memory. | +| `.trellis/scripts/` | Local Python runtime used by commands, hooks, and context injection. | +| `.trellis/.runtime/` | Session-level runtime state, such as the current task pointer. | +| `.trellis/.template-hashes.json` | Template hashes for Trellis-managed files, used by update to determine whether local files were modified by the user. | + +## AI Customization Principles + +1. **Find the local source of truth first**: Do not edit from memory. Read `.trellis/workflow.md`, `.trellis/config.yaml`, the relevant platform directory, and related task files first. +2. **Edit the user project, not the npm package cache**: Modify generated files inside the project, not `node_modules` or the global npm install directory. +3. **Keep platform files aligned with `.trellis/`**: If workflow routing changes, also check whether platform skills or commands still describe the same flow. +4. **Put project-specific rules in `.trellis/spec/` or a local skill**: Do not put team conventions into `trellis-meta`. +5. **Preserve user changes**: If a file was already modified locally, work from the current content instead of overwriting it with a default template. + +## How To Use This Directory + +- To understand which files exist after init, read `generated-files.md`. +- To change phases, routing, or next actions, read `workflow.md`. +- To change the task model, JSONL context, or active task behavior, read `task-system.md`. +- To change coding convention injection, read `spec-system.md`. +- To understand journals and cross-session memory, read `workspace-memory.md`. +- To change hooks or sub-agent context loading, read `context-injection.md`. diff --git a/.omp/skills/trellis-meta/references/local-architecture/spec-system.md b/.omp/skills/trellis-meta/references/local-architecture/spec-system.md new file mode 100644 index 000000000..61281f06f --- /dev/null +++ b/.omp/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, Phase 1.3 should write relevant specs into `implement.jsonl` / `check.jsonl`: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend conventions"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Test expectations"} +``` + +Sub-agents or platform preludes read these JSONL files and load the referenced specs. On platforms without sub-agent support, the AI should read the relevant specs directly according to the workflow. + +## What Specs Should Contain + +Specs should contain executable engineering conventions for the project, not generic best practices: + +- Where files should live. +- How error handling should be expressed. +- Input/output contracts for APIs, hooks, and commands. +- Patterns that are forbidden. +- Cases that require tests. +- Project-specific pitfalls and how to avoid them. + +When the AI learns a new rule during implementation or debugging, it should update `.trellis/spec/` rather than only summarizing it in chat. + +## Local Customization Points + +| Need | Edit location | +| --- | --- | +| Add a new spec layer | `.trellis/spec///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/.omp/skills/trellis-meta/references/local-architecture/task-system.md b/.omp/skills/trellis-meta/references/local-architecture/task-system.md new file mode 100644 index 000000000..64ad00dd6 --- /dev/null +++ b/.omp/skills/trellis-meta/references/local-architecture/task-system.md @@ -0,0 +1,101 @@ +# Local Task System + +The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. + +## Task Directory Structure + +```text +.trellis/tasks/ +├── 04-28-example-task/ +│ ├── task.json +│ ├── prd.md +│ ├── info.md +│ ├── implement.jsonl +│ ├── check.jsonl +│ └── research/ +└── archive/ + └── 2026-04/ +``` + +| File | Purpose | +| --- | --- | +| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | +| `prd.md` | Requirements document; the most important business context during implementation. | +| `info.md` | Optional technical design. | +| `implement.jsonl` | List of spec/research files the implement agent must read first. | +| `check.jsonl` | List of spec/research files the check agent must read first. | +| `research/` | Research artifacts. Complex findings should not live only in chat. | + +## `task.json` + +`task.json` records task status and metadata. Common fields: + +| Field | Meaning | +| --- | --- | +| `id` / `name` / `title` | Task identity and title. | +| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | +| `priority` | `P0`, `P1`, `P2`, `P3`. | +| `creator` / `assignee` | Creator and assignee. | +| `package` | Target package in a monorepo; may be empty. | +| `branch` / `base_branch` | Working branch and PR target branch. | +| `children` / `parent` | Parent/child task relationships. | +| `commit` / `pr_url` | Commit and PR information after completion. | +| `meta` | Extension fields. | + +The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, `prd.md`, whether JSONL context is configured, and the phase descriptions in `workflow.md`. + +## Active Task + +The user sees a "current task," but Trellis stores active task state per session. + +```text +.trellis/.runtime/sessions/.json +``` + +`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. + +If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. + +## JSONL Context + +`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. + +Format: + +```jsonl +{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} +{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} +``` + +Rules: + +- Include spec and research files. +- Do not include code files that are about to be modified. +- Do not treat temporary conclusions in chat as the only context. +- Seed rows have no `file` field; they only prompt the AI to fill in real entries. + +## Common Commands + +```bash +python3 ./.trellis/scripts/task.py create "" --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 | Phase 1.3 in `.trellis/workflow.md` and related platform agent/hook instructions. | +| Change archive policy | `.trellis/scripts/common/task_store.py` / `task_utils.py`. | + +These are local files in the user project. Do not default to editing Trellis CLI source code unless the user wants to contribute upstream. diff --git a/.omp/skills/trellis-meta/references/local-architecture/workflow.md b/.omp/skills/trellis-meta/references/local-architecture/workflow.md new file mode 100644 index 000000000..f0659ff15 --- /dev/null +++ b/.omp/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/.omp/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.omp/skills/trellis-meta/references/local-architecture/workspace-memory.md new file mode 100644 index 000000000..c2958f21b --- /dev/null +++ b/.omp/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/.omp/skills/trellis-meta/references/platform-files/agents.md b/.omp/skills/trellis-meta/references/platform-files/agents.md new file mode 100644 index 000000000..efbacfa08 --- /dev/null +++ b/.omp/skills/trellis-meta/references/platform-files/agents.md @@ -0,0 +1,79 @@ +# Agents + +Trellis agent files define specialized roles. Common Trellis agents in a user project are: + +- `trellis-research` +- `trellis-implement` +- `trellis-check` + +File locations and formats differ by platform, but responsibility boundaries should stay consistent. + +## Agent Responsibilities + +| Agent | Responsibility | +| --- | --- | +| `trellis-research` | Investigate the question and write findings into the current task's `research/`. | +| `trellis-implement` | Implement against `prd.md`, `info.md`, `implement.jsonl`, and related spec/research. | +| `trellis-check` | Review changes, fix discovered issues, and run necessary checks. | + +Agent files should not become generic chat prompts. They should define input sources, write boundaries, whether code may be changed, and how results are reported. + +## Common Paths + +| Platform | Agent path | +| --- | --- | +| Claude Code | `.claude/agents/trellis-*.md` | +| Cursor | `.cursor/agents/trellis-*.md` | +| OpenCode | `.opencode/agents/trellis-*.md` | +| Codex | `.codex/agents/trellis-*.toml` | +| Kiro | `.kiro/agents/trellis-*.json` | +| Gemini CLI | `.gemini/agents/trellis-*.md` | +| Qoder | `.qoder/agents/trellis-*.md` | +| CodeBuddy | `.codebuddy/agents/trellis-*.md` | +| Factory Droid | `.factory/droids/trellis-*.md` | +| Pi Agent | `.pi/agents/trellis-*.md` | + +GitHub Copilot agent/prompt support is provided by a combination of directories such as `.github/agents/`, `.github/prompts/`, and `.github/skills/`; inspect the files actually generated in the user project. + +Main-session workflow platforms such as Kilo, Antigravity, and Windsurf may not have Trellis sub-agent files. They usually rely on workflows/skills to guide the main session. + +## Two Context Loading Modes + +### hook push + +The platform hook injects task context before the agent starts. The agent file itself can focus more on responsibilities and boundaries. + +Common on platforms that support agent hooks. + +### agent pull + +The agent file instructs the agent to read after startup: + +- `python3 ./.trellis/scripts/task.py current --source` +- current task `prd.md` +- `info.md` +- `implement.jsonl` or `check.jsonl` +- spec/research files referenced by JSONL + +This mode fits platforms whose hooks cannot reliably rewrite sub-agent prompts. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Implement agent must follow extra restrictions | The platform's `trellis-implement` agent file. | +| Check agent must run project-specific commands | `trellis-check` agent file, and `.trellis/spec/` if needed. | +| Research agent must output a fixed format | `trellis-research` agent file. | +| Agent cannot read task context | Agent prelude or `inject-subagent-context` hook. | +| Add a project-specific agent | Platform agent directory + related workflow/command/skill entry point. | + +## Modification Principles + +1. **Keep responsibilities single-purpose**. Do not mix research, implement, and check responsibilities into one agent. +2. **Specify the read order**. Agents must know to start from the active task and then find the PRD and JSONL. +3. **Specify write boundaries**. Research usually only writes `research/`; implement can write code; check can fix issues. +4. **Keep semantics synchronized in multi-platform projects**. If the user configured Claude, Codex, and Cursor together, decide whether changes to one platform's agent also need to be applied to others. + +## Do Not Default To Editing Upstream Templates + +Local AI should default to modifying platform agent files inside the user project. Discuss upstream template source only when the user explicitly wants to contribute the change back to Trellis. diff --git a/.omp/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.omp/skills/trellis-meta/references/platform-files/hooks-and-settings.md new file mode 100644 index 000000000..94156a88d --- /dev/null +++ b/.omp/skills/trellis-meta/references/platform-files/hooks-and-settings.md @@ -0,0 +1,69 @@ +# Hooks And Settings + +Hooks/settings are the entry layer that connects a platform to Trellis. They decide which scripts, plugins, or extensions a platform runs for which events. + +## Settings Responsibilities + +settings/config files usually register: + +- session-start hook: injects a Trellis overview when a new session starts or context resets. +- workflow-state hook: parses `[workflow-state:STATUS]` blocks from `.trellis/workflow.md` and emits the body matching the current task `status` on each user input. Parser-only; the script does not embed fallback content. +- sub-agent context hook: injects task context when implementation/check/research agents start. +- shell/session bridge: lets shell commands see the same Trellis session identity. +- platform plugin or extension entry points. + +Common files: + +| Platform | settings/config | +| --- | --- | +| Claude Code | `.claude/settings.json` | +| Cursor | `.cursor/hooks.json` | +| Codex | `.codex/hooks.json`, `.codex/config.toml` | +| OpenCode | `.opencode/package.json`, `.opencode/plugins/*` | +| Kiro | `.kiro/hooks/` + platform config | +| Gemini CLI | `.gemini/settings.json` | +| Qoder | `.qoder/settings.json` | +| CodeBuddy | `.codebuddy/settings.json` | +| GitHub Copilot | `.github/copilot/hooks.json` | +| Factory Droid | `.factory/settings.json` | +| Pi Agent | `.pi/settings.json`, `.pi/extensions/trellis/` | + +Whether these files exist in a project depends on which `trellis init --<platform>` flags the user ran. + +## Hook Script Types + +| Script | Purpose | +| --- | --- | +| `session-start.py` | Generates session-start context. | +| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. | +| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. | +| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. | + +Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| AI should see more/less context in a new session | Platform `session-start` hook. | +| Per-turn hint policy should change | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The hook parses workflow.md verbatim — no script edit required. | +| Sub-agent cannot read PRD/spec | `inject-subagent-context` hook or agent prelude. | +| `task.py current` in shell has no active task | Shell/session bridge hook or platform environment variable configuration. | +| Disable an automatic injection | The corresponding hook registration in settings/config. | + +## Modification Principles + +1. **Settings wire things up; hooks define behavior**. If only the hook changes, the platform may never call it. If only settings change, behavior may not change. +2. **Confirm platform event names first**. Different platforms use different names for SessionStart, UserPromptSubmit, AgentSpawn, shell execution, and similar events. +3. **Hooks read local `.trellis/`, not upstream source**. `.trellis/scripts/` and `.trellis/workflow.md` in the user project are the default targets. +4. **Errors must be visible**. Hook failures should tell the user what was not injected instead of silently leaving the AI without context. + +## Troubleshooting Path + +If the user says "AI did not read Trellis state": + +1. Check whether the platform settings register the hook. +2. Check whether the hook file exists. +3. Manually run the `.trellis/scripts/get_context.py` or `task.py current --source` command that the hook depends on. +4. Check whether active task state exists in `.trellis/.runtime/sessions/`. +5. Check whether the platform shell passes session identity. diff --git a/.omp/skills/trellis-meta/references/platform-files/overview.md b/.omp/skills/trellis-meta/references/platform-files/overview.md new file mode 100644 index 000000000..60ae1dfb8 --- /dev/null +++ b/.omp/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/`, `.kilocode/`, `.agent/`, `.windsurf/`, and similar directories. + +Platform files do not store business state. They let the corresponding AI tool read Trellis state, call Trellis scripts, and load Trellis skills/agents/hooks. + +## Platform File Categories + +| Category | Common paths | Purpose | +| --- | --- | --- | +| settings/config | `.claude/settings.json`, `.codex/hooks.json`, `.qoder/settings.json` | Register hooks, plugins, extensions, or platform behavior. | +| hooks/plugins/extensions | `.claude/hooks/`, `.opencode/plugins/`, `.pi/extensions/` | Inject context at session start, user input, agent startup, shell execution, and similar events. | +| agents | `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` | Define `trellis-research`, `trellis-implement`, and `trellis-check`. | +| skills | `.claude/skills/`, `.agents/skills/`, `.qoder/skills/` | Capability descriptions that auto-trigger or can be read on demand. | +| commands/prompts/workflows | `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/` | Entry points explicitly invoked by the user. | + +## Three Platform Integration Modes + +### 1. Hook / Extension Driven + +These platforms can trigger scripts or plugins on specific events and actively inject Trellis context into AI. + +Common capabilities: + +- session-start injection of a `.trellis/` overview. +- workflow-state hints for each user turn. +- PRD/spec/research injection when sub-agents start. +- Shell commands inheriting session identity. + +To change "when the AI knows what," inspect hooks/plugins/extensions and settings first. + +### 2. Agent Prelude / Pull-Based + +Some platforms cannot reliably let hooks rewrite sub-agent prompts, so the agent file itself instructs the agent to read the active task, PRD, and JSONL context after startup. + +To change how sub-agents load context, inspect the agent files themselves. + +### 3. Main-Session Workflow + +Some platforms do not have Trellis sub-agent or hook capabilities. They rely on workflows/skills/commands to guide the main-session AI to read files, run scripts, and move tasks forward. + +To change behavior, inspect platform workflows/skills/commands and `.trellis/workflow.md`. + +## Local Modification Order + +When the user asks to customize behavior for a platform, the AI should inspect files in this order: + +1. Read `.trellis/workflow.md` to confirm the shared flow. +2. Read the target platform's settings/config to see which hooks/agents/skills/commands are registered. +3. Read the target platform's agents/skills/commands/hooks. +4. Modify the local file closest to the user's need. +5. If the change affects the shared flow, synchronize `.trellis/workflow.md` or `.trellis/spec/`. + +Do not modify only platform files and forget the shared workflow. Do not modify only `.trellis/workflow.md` and forget that platform entry points may still contain old descriptions. diff --git a/.omp/skills/trellis-meta/references/platform-files/platform-map.md b/.omp/skills/trellis-meta/references/platform-files/platform-map.md new file mode 100644 index 000000000..e62512905 --- /dev/null +++ b/.omp/skills/trellis-meta/references/platform-files/platform-map.md @@ -0,0 +1,91 @@ +# Platform File Map + +This page lists common Trellis file locations in a user project by platform. Whether a platform directory exists in an actual project depends on which `trellis init --<platform>` commands the user ran. + +## Matrix + +| Platform | CLI flag | Main directory | Skill directory | Agent directory | Hooks/extensions | +| --- | --- | --- | --- | --- | --- | +| Claude Code | `--claude` | `.claude/` | `.claude/skills/` | `.claude/agents/` | `.claude/hooks/` + `.claude/settings.json` | +| Cursor | `--cursor` | `.cursor/` | `.cursor/skills/` | `.cursor/agents/` | `.cursor/hooks.json` + `.cursor/hooks/` | +| OpenCode | `--opencode` | `.opencode/` | `.opencode/skills/` | `.opencode/agents/` | `.opencode/plugins/` | +| Codex | `--codex` | `.codex/` | `.agents/skills/` | `.codex/agents/` | `.codex/hooks/` + `.codex/hooks.json` | +| Kilo | `--kilo` | `.kilocode/` | `.kilocode/skills/` | Usually none | `.kilocode/workflows/` | +| Kiro | `--kiro` | `.kiro/` | `.kiro/skills/` | `.kiro/agents/` | `.kiro/hooks/` | +| Gemini CLI | `--gemini` | `.gemini/` | `.agents/skills/` | `.gemini/agents/` | `.gemini/settings.json` + `.gemini/hooks/` | +| Antigravity | `--antigravity` | `.agent/` | `.agent/skills/` | Usually none | `.agent/workflows/` | +| Windsurf | `--windsurf` | `.windsurf/` | `.windsurf/skills/` | Usually none | `.windsurf/workflows/` | +| Qoder | `--qoder` | `.qoder/` | `.qoder/skills/` | `.qoder/agents/` | `.qoder/hooks/` + `.qoder/settings.json` | +| CodeBuddy | `--codebuddy` | `.codebuddy/` | `.codebuddy/skills/` | `.codebuddy/agents/` | `.codebuddy/hooks/` + `.codebuddy/settings.json` | +| GitHub Copilot | `--copilot` | `.github/` | `.github/skills/` | `.github/agents/` | `.github/copilot/hooks/` + prompts | +| Factory Droid | `--droid` | `.factory/` | `.factory/skills/` | `.factory/droids/` | `.factory/hooks/` + settings | +| Pi Agent | `--pi` | `.pi/` | `.pi/skills/` | `.pi/agents/` | `.pi/extensions/trellis/` + `.pi/settings.json` | + +## Capability Groups + +### Trellis Sub-Agent Support + +These platforms usually have `trellis-research`, `trellis-implement`, and `trellis-check` files: + +- Claude Code +- Cursor +- OpenCode +- Codex +- Kiro +- Gemini CLI +- Qoder +- CodeBuddy +- GitHub Copilot +- Factory Droid +- Pi Agent + +When changing implementation/check/research behavior, look for the corresponding platform agent files first. + +### Main-Session Workflow Platforms + +These platforms rely more on workflows/skills to guide the main session: + +- Kilo +- Antigravity +- Windsurf + +When changing behavior, inspect workflows and skills first. Do not assume Trellis sub-agents exist. + +### Shared `.agents/skills/` + +Codex writes the shared `.agents/skills/` layer. Some tools that support agentskills.io can also read this directory. If the user wants multiple compatible tools to share one skill, consider `.agents/skills/` first, but do not assume every platform reads it. + +## Decision Rules When Modifying Platform Files + +1. User specified a platform: modify only that platform directory unless shared workflow/spec files must also change. +2. User says "all platforms should do this": synchronize equivalent entry points platform by platform; do not modify only one directory. +3. User only says "my AI": inspect the configuration directories that actually exist in the project and infer the current AI platform. +4. User wants project rules: prefer `.trellis/spec/` or a project-local skill. +5. User wants Trellis behavior: edit `.trellis/workflow.md` plus platform hooks/agents/skills/commands. + +## When Paths Differ + +Platform ecosystems change, and user projects may already be customized. If this table disagrees with local files, use the actual settings/config in the user project as authoritative: + +- Check the hook that settings registers. +- Check the script that a command/prompt/workflow points to. +- Judge behavior by the read rules currently written in the agent file. + +Do not delete a custom file just because it is not listed in this path table. + +### `.omp/` — Oh My Pi (OMP) + +Extension-backed platform. OMP native provider auto-discovers all subdirectories. + +``` +.omp/ +├── commands/ # Slash commands (flat .md) +├── skills/ # Auto-triggered skills (SKILL.md per dir) +├── agents/ # Agent definitions (.md) +└── extensions/ + └── trellis/ + └── index.ts # Trellis extension (context injection) +``` + +No `settings.json` — OMP scans `.omp/` subdirectories automatically. +No Python hooks — hook-equivalent behavior lives in the TypeScript extension. diff --git a/.omp/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.omp/skills/trellis-meta/references/platform-files/skills-and-commands.md new file mode 100644 index 000000000..816c666a8 --- /dev/null +++ b/.omp/skills/trellis-meta/references/platform-files/skills-and-commands.md @@ -0,0 +1,83 @@ +# Skills, Commands, Prompts, And Workflows + +Skills and commands are textual entry points for user interaction with Trellis. Different platforms use different names, but their core purpose is the same: tell the AI how to enter the Trellis flow when the user expresses a certain intent. + +## Conceptual Differences + +| Type | Trigger mode | Best for | +| --- | --- | --- | +| skill | AI auto-match or explicit user mention | Long-term capabilities, workflow rules, modification guides. | +| command | Explicit user invocation | Clear operation entry points such as continue and finish-work. | +| prompt | Explicit user invocation or platform selection | Similar to command, but in a platform prompt format. | +| workflow | Explicit user selection or platform auto-match | Guides the main session when no sub-agent/hook exists. | + +Trellis workflow skills usually share one semantic set: brainstorm, before-dev, check, update-spec, break-loop. Multi-file built-in skills such as `trellis-meta` use layered references. + +## Common Paths + +| Platform | Common entries | +| --- | --- | +| Claude Code | `.claude/skills/`, `.claude/commands/` | +| Cursor | `.cursor/skills/`, `.cursor/commands/` | +| OpenCode | `.opencode/skills/`, `.opencode/commands/` | +| Codex | `.agents/skills/`, `.codex/skills/` | +| Kilo | `.kilocode/skills/`, `.kilocode/workflows/` | +| Kiro | `.kiro/skills/` | +| Gemini CLI | `.agents/skills/`, `.gemini/commands/` | +| Antigravity | `.agent/skills/`, `.agent/workflows/` | +| Windsurf | `.windsurf/skills/`, `.windsurf/workflows/` | +| Qoder | `.qoder/skills/`, `.qoder/commands/` | +| CodeBuddy | `.codebuddy/skills/`, `.codebuddy/commands/` | +| GitHub Copilot | `.github/skills/`, `.github/prompts/` | +| Factory Droid | `.factory/skills/`, `.factory/commands/` | +| Pi Agent | `.pi/skills/` | + +In a user project, use the files actually generated by init as authoritative. + +## Skill Structure + +A common skill is a directory: + +```text +trellis-meta/ +├── SKILL.md +└── references/ +``` + +`SKILL.md` should tell the AI: + +- When to use this skill. +- Which reference to read first for the current task. +- What not to do. + +References hold longer explanations so the entry file does not contain everything. + +## Command/Prompt/Workflow Structure + +Commands, prompts, and workflows are usually single files. Their content should include: + +- When to use it. +- Which `.trellis/` files to read. +- Which scripts to run. +- How to report after completion. + +They should not store task state; task state belongs in `.trellis/tasks/` and `.trellis/.runtime/`. + +## Local Change Scenarios + +| User need | Edit location | +| --- | --- | +| Change AI auto-trigger rules | The corresponding skill's frontmatter description. | +| Change user command behavior | The corresponding command/prompt/workflow file. | +| Add a project-local skill | Platform skill directory, or shared `.agents/skills/`. | +| Let multiple platforms share one capability | Write equivalent skills in each platform skill directory, or use the `.agents/skills/` shared layer on platforms that support it. | +| Change finish/continue entry points | Platform commands/prompts/workflows. | + +## Modification Principles + +1. **Keep entry files short; references carry long content**. This matters especially for multi-file skills like `trellis-meta`. +2. **Make trigger descriptions specific**. A description that is too broad can mis-trigger; one that is too narrow may not trigger. +3. **Keep the same semantics consistent across platforms**. File formats can differ, but behavior descriptions should match. +4. **Put project-specific capabilities in local skills**. Do not put team-private flows into public `trellis-meta`. + +If the user only wants local AI to know one more project rule, usually create a project-local skill or update `.trellis/spec/` instead of changing a Trellis built-in workflow skill. diff --git a/.omp/skills/trellis-spec-bootstarp/SKILL.md b/.omp/skills/trellis-spec-bootstarp/SKILL.md new file mode 100644 index 000000000..2f7c7ad8f --- /dev/null +++ b/.omp/skills/trellis-spec-bootstarp/SKILL.md @@ -0,0 +1,41 @@ +--- +name: trellis-spec-bootstarp +description: "Bootstrap project-specific Trellis coding specs with a platform-neutral single-agent workflow. Use when creating or refreshing .trellis/spec guidelines, analyzing a codebase with GitNexus, ABCoder, or source inspection, decomposing package/layer spec work, and writing real codebase-backed spec docs without placeholder text." +--- + +# Trellis Spec Bootstarp + +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/.omp/skills/trellis-spec-bootstarp/references/mcp-setup.md b/.omp/skills/trellis-spec-bootstarp/references/mcp-setup.md new file mode 100644 index 000000000..629fcbda8 --- /dev/null +++ b/.omp/skills/trellis-spec-bootstarp/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/.omp/skills/trellis-spec-bootstarp/references/repository-analysis.md b/.omp/skills/trellis-spec-bootstarp/references/repository-analysis.md new file mode 100644 index 000000000..1309d293d --- /dev/null +++ b/.omp/skills/trellis-spec-bootstarp/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/.omp/skills/trellis-spec-bootstarp/references/spec-task-planning.md b/.omp/skills/trellis-spec-bootstarp/references/spec-task-planning.md new file mode 100644 index 000000000..dca26871a --- /dev/null +++ b/.omp/skills/trellis-spec-bootstarp/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/.omp/skills/trellis-spec-bootstarp/references/spec-writing.md b/.omp/skills/trellis-spec-bootstarp/references/spec-writing.md new file mode 100644 index 000000000..6bc7dec82 --- /dev/null +++ b/.omp/skills/trellis-spec-bootstarp/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/.omp/skills/trellis-update-spec/SKILL.md b/.omp/skills/trellis-update-spec/SKILL.md new file mode 100644 index 000000000..557bc4ee7 --- /dev/null +++ b/.omp/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/scripts/common/cli_adapter.py b/.trellis/scripts/common/cli_adapter.py index 99f46d81b..d6363d0b2 100755 --- a/.trellis/scripts/common/cli_adapter.py +++ b/.trellis/scripts/common/cli_adapter.py @@ -53,6 +53,7 @@ "copilot", "droid", "pi", + "omp", ] @@ -127,6 +128,8 @@ def config_dir_name(self) -> str: return ".factory" elif self.platform == "pi": return ".pi" + elif self.platform == "omp": + return ".omp" else: return ".claude" @@ -184,6 +187,17 @@ def get_commands_path(self, project_root: Path, *parts: str) -> Path: filename = filename[:-3] return prompts_dir / f"trellis-{filename}.md" return prompts_dir / Path(*parts) + if self.platform == "omp": + commands_dir = self.get_config_dir(project_root) / "commands" + if not parts: + return commands_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return commands_dir / f"trellis-{filename}.md" + return commands_dir / Path(*parts) + if self.platform == "devin": workflow_dir = self.get_config_dir(project_root) / "workflows" @@ -268,6 +282,8 @@ def get_trellis_command_path(self, name: str) -> str: return f".factory/commands/trellis/{name}.md" elif self.platform == "pi": return f".pi/prompts/trellis-{name}.md" + elif self.platform == "omp": + return f".omp/commands/trellis-{name}.md" else: return f"{self.config_dir_name}/commands/trellis/{name}.md" @@ -305,6 +321,8 @@ def get_non_interactive_env(self) -> dict[str, str]: return {} elif self.platform == "pi": return {} + elif self.platform == "omp": + return {} else: return {"CLAUDE_NON_INTERACTIVE": "1"} @@ -390,6 +408,10 @@ def build_run_command( ) elif self.platform == "pi": cmd = ["pi", "-p", prompt] + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI agent run is not supported." + ) else: # claude cmd = ["claude", "-p"] @@ -456,6 +478,10 @@ def build_resume_command(self, session_id: str) -> list[str]: ) elif self.platform == "pi": return ["pi", "-c", session_id] + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI resume is not supported." + ) else: return ["claude", "--resume", session_id] @@ -530,6 +556,8 @@ def cli_name(self) -> str: return "droid" elif self.platform == "pi": return "pi" + elif self.platform == "omp": + return "omp" else: return "claude" @@ -625,9 +653,10 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: "copilot", "droid", "pi", + "omp", ): raise ValueError( - f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')" + f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or 'omp')" ) return CLIAdapter(platform=platform) # type: ignore @@ -650,6 +679,7 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: ".github/copilot", ".factory", ".pi", + ".omp", ) """Platform-specific config directory names used by detect_platform exclusion checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform @@ -715,6 +745,7 @@ def detect_platform(project_root: Path) -> Platform: "copilot", "droid", "pi", + "omp", ): return env_platform # type: ignore @@ -791,6 +822,10 @@ def detect_platform(project_root: Path) -> Platform: if (project_root / ".pi").is_dir(): return "pi" + # Check for .omp directory (OMP-specific) + if (project_root / ".omp").is_dir(): + return "omp" + # Fallback: checkout only has the Codex shared-skills layer # (.agents/skills/trellis-* dirs) and no explicit platform config dir. # Happens on fresh clones where .codex/ is gitignored/absent but the diff --git a/.trellis/scripts/common/task_store.py b/.trellis/scripts/common/task_store.py index 8a0e4ebd3..92fb39a4e 100755 --- a/.trellis/scripts/common/task_store.py +++ b/.trellis/scripts/common/task_store.py @@ -128,6 +128,7 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str: ".factory", # Factory Droid ".github/copilot", ".pi", # Pi Agent + ".omp", # Oh My Pi ) _SEED_EXAMPLE = ( diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl new file mode 100644 index 000000000..c4533637f --- /dev/null +++ b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl @@ -0,0 +1,5 @@ +{"_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."} +{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform integration patterns - verify consistency"} +{"file": ".trellis/spec/cli/backend/error-handling.md", "reason": "Error handling conventions for quality check"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Unit test conventions for verification"} +{"file": ".trellis/spec/cli/unit-test/index.md", "reason": "Unit test overview for verification"} diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl new file mode 100644 index 000000000..ce49f2c2e --- /dev/null +++ b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl @@ -0,0 +1,7 @@ +{"_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."} +{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform integration patterns for adding new AI_TOOLS entry"} +{"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend architecture overview"} +{"file": ".trellis/spec/cli/backend/directory-structure.md", "reason": "CLI directory structure conventions"} +{"file": ".trellis/spec/guides/cross-platform-thinking-guide.md", "reason": "Cross-platform thinking patterns for new platform support"} +{"file": ".trellis/tasks/05-22-add-omp-platform-support-to-trellis/research/omp-capabilities.md", "reason": "OMP platform capability details"} +{"file": ".trellis/tasks/05-22-add-omp-platform-support-to-trellis/research/OMP.md", "reason": "OMP integration design plan"} diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md new file mode 100644 index 000000000..c65771f82 --- /dev/null +++ b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md @@ -0,0 +1,278 @@ +# PRD: Add OMP (Oh My Pi) Platform Support to Trellis + +## 1. 概述 + +在 Trellis CLI 中新增 `omp` 作为第 15 个 AI 编程平台。用户执行 `trellis init --omp` 时,CLI 在项目根生成 `.omp/` 下的所有必要文件,使 OMP 能通过原生 provider 自动发现并加载 Trellis 的工作流系统。 + +OMP 是 Pi 的进化版本,拥有更先进的 ExtensionAPI、原生 task tool、多源能力发现系统。OMP 原生覆盖了 Pi 插件中最复杂的部分(sub-agent 派生、bash 上下文注入、Agent 定义加载),使 Trellis 集成代码量相比 Pi 减少约 65%(~1173 行 → ~400 行)。 + +OMP extension(`.omp/extensions/trellis/index.ts`)需实现约 280 行 TypeScript,核心组件: + +- `resolveActiveTaskStatus()` — 读取 `.trellis/.runtime/sessions/` 解析活跃任务状态 +- `buildTaskContext()` — 读取 prd.md + info.md + JSONL 引用的 spec/research 文件 +- `buildSessionOverview()` — spawn `python3 get_context.py --mode session-overview`,5s 超时,失败回退静态文本(必须使用 `--mode session-overview` 以满足 spec Cross-platform consistency invariant 的 parity 要求) +- `TurnContextCache` — 1.5s TTL 防止同 turn 的 `input` + `before_agent_start` 双重 spawn +- `before_agent_start` handler — 注入 workflow-state + task context + session overview +- `input` handler — per-turn 刷新 workflow-state + session overview + +## 2. 背景 + +Trellis 已支持 14 个 AI 编程平台(Claude Code、Cursor、Codex、Gemini、Pi 等)。每个平台通过统一的 `AI_TOOLS` 注册表 + `configurators/` 调度机制接入。 + +**OMP vs Pi 架构差异决定插件策略**: + +| | Pi | OMP | +|---|---|---| +| Extension 运行位置 | 独立 wrapper 进程 | OMP 进程内 | +| Sub-agent 机制 | 插件自己 spawn `pi --mode text`(~500 行) | OMP 原生 task tool | +| 上下文注入方式 | `systemPrompt` 字段直接拼接 | `customType` custom message | +| 跨进程上下文 | 需要 TRELLIS_CONTEXT_ID 环境变量 + tool_call 拦截 | 单进程内文件系统直接读,不需要 | + +这些差异意味着移植 Pi 逻辑时要**简化而非照搬**:context key 管理链路(`resolveContextKey` / `adoptExistingContextKey` / `createProcessContextKey`)全部可省略,sub-agent 工具注册和 bash 拦截也不需要。但核心的上下文注入逻辑(任务内容 + session overview)必须保留,不能因 OMP 的便利性而省略。 + +相关规范: +- `.trellis/spec/cli/backend/platform-integration.md` — 平台接入架构与检查清单 + +## 3. 目标与范围 + +### 目标 +- `trellis init --omp` 生成正确的 `.omp/` 目录结构(commands、skills、agents、extension) +- `trellis update` 正确追踪 OMP 产物文件 +- OMP 启动后自动发现 Trellis extension 并加载所有能力 +- AI 在每轮 turn 获得 `<workflow-state>` breadcrumb,根据阶段触发对应 skill +- **AI 获得当前任务上下文**(prd.md + info.md + jsonl spec context),通过 `before_agent_start` 以 custom message 注入 +- **AI 获得完整 session overview**(`get_context.py` 输出),包含开发者信息、git 分支、活跃任务摘要 +- **添加 `input` handler**,确保 per-turn 注入的 breadcrumb 和 session overview 在长对话中保持新鲜 +- AI 通过 OMP 原生 `task` tool 调用 `trellis-implement` / `trellis-check` / `trellis-research` 子 agent + +### 非目标 (MVP) +- TTSR 规则(流式拦截工作流违规) +- 自定义 slash command(如 `/trellis:status`) +- `tool_result` 后处理(截断、注入链接) +- `context` 事件 handler(Pi 的 passthrough 本身是空操作,无需移植) +- `tool_call` 事件 handler(OMP 单进程内不需要 TRELLIS_CONTEXT_ID 环境变量注入) +- Sub-agent 工具注册 `registerTool("subagent", ...)`(OMP 原生 task tool 覆盖) +- Agent 定义加载(`.pi/agents/*.md`)(OMP skills/agents 自动发现覆盖) + +## 4. 关键设计决策 + +### 4.1 不生成 `settings.json` + +OMP 的 native provider 在启动时自动扫描 `.omp/` 下的标准子目录(`extensions/`、`commands/`、`skills/`、`agents/`),无需任何显式配置。这与 Pi 不同,Pi 需要在 `settings.json` 中显式声明所有能力路径和 npm 包依赖。 + +### 4.2 Extension 核心功能 + +OMP extension 实现以下注入能力(无需子 agent 调度和 bash 拦截,OMP 原生 task tool 覆盖): + +| 功能 | Pi extension (1173 行) | OMP extension (~400 行) | +|---|---|---| +| Workflow-state 面包屑 | before_agent_start + input | before_agent_start + input | +| 任务上下文(prd/info/jsonl) | before_agent_start systemPrompt 拼接 | before_agent_start custom message | +| Session overview | get_context.py 子进程 | get_context.py 子进程 | +| Sub-agent 调度 | 500+ 行 spawn 逻辑 | OMP task tool 原生处理 | +| Bash 上下文注入 | tool_call 拦截 TRELLIS_CONTEXT_ID | 不需要(单进程) | +| Context key 管理 | resolveContextKey + adoptExistingContextKey 链路 | 不需要(单进程) | + +与 Pi 的关键简化: +- **不需要跨进程 context key**:OMP extension 运行在 OMP 进程内,直接通过文件系统读取 session 文件获取当前任务,无需 `TRELLIS_CONTEXT_ID` 环境变量传递链路 +- **不需要 sub-agent 工具注册**:OMP 原生 `task` tool 覆盖 sub-agent 派生,不需要 `registerTool("subagent")` + `runPi()` spawn 逻辑 +- **注入方式改为 custom message**:OMP ExtensionAPI 不支持 `systemPrompt` 直接拼接,改为 `customType` custom message 注入,对模型行为的引导效果相当 + +### 4.3 Agent 不使用 pull-based prelude + +OMP 是真正的 class-1 平台:task tool 子 session 会重新加载 extensions,`before_agent_start` 自动触发并注入 breadcrumb。不需要 class-2 平台的 pull-based prelude 补偿机制。 + +### 4.4 Agent 工具名使用 OMP 原生命名 + +| Pi 工具名 | OMP 工具名 | +|---|---| +| Read, Write, Edit, Bash | read, write, edit, bash | +| Glob, Grep | find, search | +| (不存在) | ast_grep, lsp | + +### 4.5 不生成 `start` 命令 + +`agentCapable: true` → `filterCommands()` 自动过滤 `start`。Extension 在 session 启动时自动注入 workflow 上下文。 + +## 5. 实现计划 + +分两阶段执行:**Phase 1** 先手动生成 `.omp/` 产物文件用于 OMP 端验证,验证通过后 **Phase 2** 再适配 Trellis CLI 使其通过 `trellis init --omp` 自动化生成。 + +### Phase 1: 手动生成 .omp/ 产物(先行验证) + +直接在 Trellis 项目根创建 `.omp/` 目录及所有文件,用于在 OMP 中实际测试: + +1. **commands** — 2 个斜杠命令 +2. **skills** — 5 个技能 + 2 个多文件技能 +3. **agents** — 3 个 agent 定义 +4. **extensions** — 1 个 extension 入口 + +目标:确认 OMP 能正确加载 extension、技能被发现、agent 可通过 task tool 调用、workflow-state breadcrumb 注入生效。 + +验证通过后进入 Phase 2。 + +### Phase 2: 适配 Trellis CLI + +#### 5.2.1 `types/ai-tools.ts` — 新增 OMP 平台注册 + +```typescript +omp: { + name: "Oh My Pi", + templateDirs: ["common", "omp"], + configDir: ".omp", + cliFlag: "omp", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, // class-1: extension 注入 context + hasHooks: true, + cliFlag: "omp", + }, +}, +``` + +`AITool`、`TemplateDir`、`CliFlag` 联合类型各增加 `"omp"` 字面量。 + +#### 5.2.2 `templates/omp/` — 新建模板目录 + +将 Phase 1 验证通过的产物文件搬入模板目录: + +``` +templates/omp/ +├── index.ts # 导出 getAllAgents() + getExtensionTemplate() +├── agents/ +│ ├── trellis-implement.md # OMP 原生工具名, 无 pull-based prelude +│ ├── trellis-check.md # OMP 原生工具名, 无 pull-based prelude +│ └── trellis-research.md # 含 web_search 工具 +└── extensions/ + └── trellis/ + └── index.ts.txt # ExtensionAPI 模板源(.txt 后缀防止 tsc 编译,与 Pi 一致) +``` + +> **重要约定**:Extension 模板文件必须使用 `.ts.txt` 后缀(参照 Pi 的 `index.ts.txt`),原因: +> 1. 防止 `tsc` 在 production build 时将其编译为 `.js` 并输出到 `dist/templates/omp/extensions/` +> 2. 与 Pi 模式保持一致(spec `platform-integration.md` Step 4 "TypeScript extension pattern" 明确要求 `.ts.txt`) +> 3. `readTemplate("extensions/trellis/index.ts.txt")` 读取,configurator 写入时去掉 `.txt` 后缀 + +#### 5.2.3 `configurators/omp.ts` — 新建配置器 + +``` +export configureOmp(cwd): 写入 commands、skills、agents、extension +export collectOmpTemplates(): 返回所有产物文件 Map (供 trellis update 哈希比对) +``` + +参照 Pi 配置器,简化点: +- 无 `settings.json` +- Agent 不调用 `applyPullBasedPreludeMarkdown` +- 命令输出到 `.omp/commands/`(非 `.pi/prompts/`) + +> **关键规则:`collectOmpTemplates()` 必须对 extension 模板应用与 `configureOmp()` 相同的内容变换**。 +> 具体而言:`configureOmp()` 写入 extension 时调用 `replacePythonCommandLiterals(getExtensionTemplate())`,`collectOmpTemplates()` 返回同一文件内容时也必须包裹 `replacePythonCommandLiterals()`。 +> 否则 Windows 上 `trellis update` 每次运行都会误报 extension 文件变更(因为 `python3` vs `python` 不一致导致哈希不匹配)。 +> 参见 spec `platform-integration.md` → Common Mistakes → "Template placeholder not resolved in collectTemplates"。 + +#### 5.2.4 `configurators/index.ts` — 注册 OMP + +```typescript +PLATFORM_FUNCTIONS.omp = { + configure: configureOmp, + collectTemplates: () => collectOmpTemplates(), +}; +``` + +#### 5.2.5 CLI + InitOptions + +- `cli/index.ts`:添加 `--omp` flag +- `commands/init.ts`:`InitOptions` 添加 `omp?: boolean` + +#### 5.2.6 Extension 核心逻辑 (`templates/omp/extensions/trellis/index.ts`) + +``` +session_start → 发现项目根 (.trellis 存在) → 通知 UI +before_agent_start → 注入 workflow-state breadcrumb + task context (prd/info/jsonl) + session overview +input → per-turn 刷新 workflow-state + session overview (TurnContextCache 1.5s TTL) +``` + +约 280 行 TypeScript。依赖关系:`ExtensionAPI` from `@oh-my-pi/pi-coding-agent`。 + +## 6. 产物文件完整清单 + +``` +.omp/ +├── commands/ +│ ├── trellis-continue.md +│ └── trellis-finish-work.md +├── skills/ +│ ├── trellis-brainstorm/SKILL.md +│ ├── trellis-before-dev/SKILL.md +│ ├── trellis-check/SKILL.md +│ ├── trellis-break-loop/SKILL.md +│ ├── trellis-update-spec/SKILL.md +│ ├── trellis-meta/... (多文件) +│ └── trellis-spec-bootstarp/... (多文件) +├── agents/ +│ ├── trellis-implement.md +│ ├── trellis-check.md +│ └── trellis-research.md +└── extensions/ + └── trellis/ + └── index.ts +``` + +## 7. 风险与缓解 + +| 风险 | 缓解 | +|---|---| +| `findAllNearestProjectConfigDirs` walk-up 行为导致 `.omp/` 在错误层级生成 | 确保 init 时 cwd 为 workspace 根 | +| 用户已禁用多个 provider(opencode, codex, gemini, cursor),但 native provider 未被禁用 | 验证 native provider 仍加载 `.omp/` 内容 | +| `skills.enablePiUser` / `enablePiProject` 遗留开关可能 gate native skill source | 文档提醒用户检查此项 | +| `ast_grep` 工具名是否与 OMP 实际注册名一致 | 在真实 OMP 环境验证 agent 工具列表 | +| Agent 发现是每次执行时重新扫描,模板变更可能不生效 | 需 OMP session 重载才能捕获 agent 变更 | + +## 8. 验证计划 + +### Phase 1 验证 (OMP 端) +1. 手动创建 `.omp/` 目录结构,检查产物文件内容格式正确 +2. 启动 OMP,确认 extension 加载且技能被发现 +3. AI 获得 `<workflow-state>` breadcrumb,能根据阶段触发对应 skill +4. AI 能通过 `task` tool 调用 `trellis-implement` / `trellis-check` / `trellis-research` + +### Phase 2 验证 (CLI 端) +5. `trellis init --omp` 生成与 Phase 1 一致的 `.omp/` 目录结构 +6. `trellis update` 正确追踪 OMP 产物文件 +7. 回归验证:Phase 1 中通过的 OMP 端测试在 `trellis init --omp` 产物上再次通过 + +## 9. 工作量估算 + +### Phase 1 +| 组件 | 内容 | 复杂度 | +|---|---|---| +| `.omp/commands/` | 2 个命令文件 | 低 | +| `.omp/skills/` | 5 个技能 + 2 个多文件技能 | 低 | +| `.omp/agents/` | 3 个 agent 定义 | 低 | +| `.omp/extensions/trellis/index.ts` | extension 入口 | 中 | +| **Phase 1 总计** | — | — | + +### Phase 2 +| 组件 | 代码量 | 复杂度 | +|---|---|---| +| `types/ai-tools.ts` 修改 | 3 行 + 1 条目 | 低 | +| `configurators/omp.ts` | ~80 行 | 中 | +| `templates/omp/` (从 Phase 1 产物搬入) | 6+ 文件 | 低 | +| `templates/omp/index.ts` | ~15 行 | 低 | +| `configurators/index.ts` | 4 行 | 低 | +| CLI + InitOptions | ~10 行 | 低 | +| **Phase 2 总计** | **~110 行新代码** + 模板搬运 | — | + +## 10. 开发命令 + +```bash +# 构建(在 packages/cli 下执行) +cd packages/cli && pnpm run build + +# 一次性测试(回到仓库根目录执行,不影响已安装的 trellis) +cd ../.. && node ./packages/cli/bin/trellis.js init --omp +``` diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json new file mode 100644 index 000000000..19da5cba8 --- /dev/null +++ b/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json @@ -0,0 +1,26 @@ +{ + "id": "add-omp-platform-support-to-trellis", + "name": "add-omp-platform-support-to-trellis", + "title": "Add OMP platform support to Trellis", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "--name", + "assignee": "--name", + "createdAt": "2026-05-22", + "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 index d7c53eb94..21e16c72e 100644 --- a/.trellis/workflow.md +++ b/.trellis/workflow.md @@ -183,7 +183,7 @@ Complex task: ask the user if you can create a Trellis task and enter the planni - 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 (sub-agent-dispatch platforms only; inline platforms skip) +- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi (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 @@ -272,7 +272,7 @@ Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first. When a user request matches one of these intents inside an active task, route first, then load the detailed phase step if needed. -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] - Planning or unclear requirements -> `trellis-brainstorm`. - `in_progress` implementation/check -> dispatch `trellis-implement` / `trellis-check`. @@ -353,7 +353,7 @@ Return to this step whenever requirements change and revise the relevant artifac Research can happen at any time during requirement exploration. It isn't limited to local code — you can use any available tool (MCP servers, skills, web search, etc.) to look up external information, including third-party library docs, industry practices, API references, etc. -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] Spawn the research sub-agent: @@ -380,7 +380,7 @@ Brainstorm and research can interleave freely — pause to research a technical #### 1.3 Configure context `[required · once]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] 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. @@ -425,7 +425,7 @@ Ready gate: both `implement.jsonl` and `check.jsonl` must contain at least one r 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] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] [codex-inline, Kilo, Antigravity, Devin] @@ -458,7 +458,7 @@ If `task.py start` errors with a session-identity message (no context key from h | `design.md` exists (complex tasks) | ✅ | | `implement.md` exists (complex tasks) | ✅ | -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] | `implement.jsonl` and `check.jsonl` each contain at least one real curated entry (seed row does not count) | ✅ | @@ -472,7 +472,7 @@ Goal: turn reviewed planning artifacts into code that passes quality checks. #### 2.1 Implement `[required · repeatable]` -[Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] Spawn the implement sub-agent: @@ -526,7 +526,7 @@ The platform prelude auto-handles the context load requirement: #### 2.2 Quality check `[required · repeatable]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi] Spawn the check sub-agent: diff --git a/README.md b/README.md index 9c8453981..7a11abb48 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ <p align="center"> <strong>An out-of-the-box engineering framework for AI coding.</strong><br/> -<sub>AI writes code fast, but every session it starts from scratch — no memory of your project, your conventions, or your team's requirements. Trellis persists specs, tasks, and memory into your repo, so any coding agent works to your engineering standards.</sub> +<sub>AI writes code fast, but every session it starts from scratch — no memory of your project, your conventions, or your team's requirements. Trellis persists specs, tasks, and memory into your repo, so any coding agent works to your engineering standards. Supports Claude Code, Cursor, OpenCode, Codex, Kiro, Kilo, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Droid, Pi Agent, Reasonix, and Oh My Pi.</sub> </p> <p align="center"> @@ -44,7 +44,7 @@ | **Task-centered workflow** | Keep PRDs, implementation context, review context, and task status in `.trellis/tasks/` so AI work stays structured. | | **Project memory** | Journals in `.trellis/workspace/` preserve what happened last time, so each new session starts with real context. | | **Team-shared standards** | Specs live in the repo, so one person's hard-won workflow or rule can benefit the whole team. | -| **Multi-platform setup** | Bring the same Trellis structure to 16 AI coding platforms instead of rebuilding your workflow per tool. | +| **Multi-platform setup** | Bring the same Trellis structure to 17 AI coding platforms instead of rebuilding your workflow per tool. | ## Prerequisites: diff --git a/README_CN.md b/README_CN.md index d166b9a2e..df9a3edb1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,7 @@ <p align="center"> <strong>开箱即用的 AI 编码工程化框架</strong><br/> -<sub>AI 写代码很快,但它每次会话都从零开始理解项目,记不住你的规范,也记不住团队级别的需求。Trellis 会把规范、任务、记忆沉淀进仓库,让任意 Coding Agent 都按你的工程标准来实践。</sub> +<sub>AI 写代码很快,但它每次会话都从零开始理解项目,记不住你的规范,也记不住团队级别的需求。Trellis 会把规范、任务、记忆沉淀进仓库,让任意 Coding Agent 都按你的工程标准来实践。支持 Claude Code、Cursor、OpenCode、Codex、Kiro、Kilo、Gemini CLI、Antigravity、Windsurf、Qoder、CodeBuddy、GitHub Copilot、Droid、Pi Agent、Reasonix 和 Oh My Pi。</sub> </p> <p align="center"> @@ -44,7 +44,7 @@ | **任务驱动工作流** | PRD、实现上下文、审查上下文与任务状态统一存放于 `.trellis/tasks/`,AI 开发过程保持结构化、可追溯。 | | **项目记忆** | `.trellis/workspace/` 中的工作日志(journal)会保留上一次会话的脉络,因此每次新会话都能基于真实上下文开始。 | | **团队共享标准** | Spec 随仓库一同版本化,个人总结出的规则与流程可以直接成为整个团队的基础设施。 | -| **多平台复用** | 同一套 Trellis 结构覆盖 16 个 AI coding 平台,无需为每个工具单独搭建工作流。 | +| **多平台复用** | 同一套 Trellis 结构覆盖 17 个 AI coding 平台,无需为每个工具单独搭建工作流。 | ## 前置要求 diff --git a/packages/cli/src/configurators/omp.ts b/packages/cli/src/configurators/omp.ts new file mode 100644 index 000000000..ca29bd474 --- /dev/null +++ b/packages/cli/src/configurators/omp.ts @@ -0,0 +1,86 @@ +import path from "node:path"; +import { AI_TOOLS } from "../types/ai-tools.js"; +import { ensureDir, writeFile } from "../utils/file-writer.js"; +import { + collectSkillTemplates, + replacePythonCommandLiterals, + resolveCommands, + resolveBundledSkills, + resolveSkills, + writeAgents, + writeSkills, +} from "./shared.js"; +import { getAllAgents, getExtensionTemplate } from "../templates/omp/index.js"; + +/** + * Collect all OMP template files for `trellis update` hash comparison. + * OMP has no settings.json — the native provider auto-discovers all capabilities. + */ +export function collectOmpTemplates(): Map<string, string> { + const config = AI_TOOLS.omp; + const ctx = config.templateContext; + const files = new Map<string, string>(); + + // Commands → .omp/commands/ + for (const command of resolveCommands(ctx)) { + files.set(`.omp/commands/trellis-${command.name}.md`, command.content); + } + + // Skills → .omp/skills/ + for (const [filePath, content] of collectSkillTemplates( + ".omp/skills", + resolveSkills(ctx), + resolveBundledSkills(ctx), + )) { + files.set(filePath, content); + } + + // Agents (class-1: no pull-based prelude) + for (const agent of getAllAgents()) { + files.set(`.omp/agents/${agent.name}.md`, agent.content); + } + + // Extension + files.set( + ".omp/extensions/trellis/index.ts", + replacePythonCommandLiterals(getExtensionTemplate()), + ); + + return files; +} + +/** + * Initialize OMP platform: write commands, skills, agents, and extension. + * No settings.json — OMP's native provider auto-discovers all subdirectories. + */ +export async function configureOmp(cwd: string): Promise<void> { + const config = AI_TOOLS.omp; + const ctx = config.templateContext; + const configRoot = path.join(cwd, config.configDir); + + // Commands + ensureDir(path.join(configRoot, "commands")); + for (const command of resolveCommands(ctx)) { + await writeFile( + path.join(configRoot, "commands", `trellis-${command.name}.md`), + command.content, + ); + } + + // Skills + await writeSkills( + path.join(configRoot, "skills"), + resolveSkills(ctx), + resolveBundledSkills(ctx), + ); + + // Agents (class-1: no applyPullBasedPreludeMarkdown) + await writeAgents(path.join(configRoot, "agents"), getAllAgents()); + + // Extension + ensureDir(path.join(configRoot, "extensions", "trellis")); + await writeFile( + path.join(configRoot, "extensions", "trellis", "index.ts"), + replacePythonCommandLiterals(getExtensionTemplate()), + ); +} diff --git a/packages/cli/src/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md b/packages/cli/src/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md index dddb74382..6d6b99ddb 100644 --- a/packages/cli/src/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +++ b/packages/cli/src/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md @@ -86,3 +86,20 @@ Platform ecosystems change, and user projects may already be customized. If this - Judge behavior by the read rules currently written in the agent file. Do not delete a custom file just because it is not listed in this path table. + +### `.omp/` — Oh My Pi (OMP) + +Extension-backed platform. OMP native provider auto-discovers all subdirectories. + +``` +.omp/ +├── commands/ # Slash commands (flat .md) +├── skills/ # Auto-triggered skills (SKILL.md per dir) +├── agents/ # Agent definitions (.md) +└── extensions/ + └── trellis/ + └── index.ts # Trellis extension (context injection) +``` + +No `settings.json` — OMP scans `.omp/` subdirectories automatically. +No Python hooks — hook-equivalent behavior lives in the TypeScript extension. diff --git a/packages/cli/src/templates/omp/agents/trellis-check.md b/packages/cli/src/templates/omp/agents/trellis-check.md new file mode 100644 index 000000000..098265f9a --- /dev/null +++ b/packages/cli/src/templates/omp/agents/trellis-check.md @@ -0,0 +1,41 @@ +--- +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, search, ast_grep, lsp +--- + +# 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 via the `task` tool. +- If injected workflow-state breadcrumbs 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 and follow the spec and research files listed in the task's `check.jsonl`. +3. Review all changed code against the task PRD and project specs. +4. Fix issues directly when they are within scope. +5. Run the relevant lint, typecheck, and focused tests 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/packages/cli/src/templates/omp/agents/trellis-implement.md b/packages/cli/src/templates/omp/agents/trellis-implement.md new file mode 100644 index 000000000..a022524c2 --- /dev/null +++ b/packages/cli/src/templates/omp/agents/trellis-implement.md @@ -0,0 +1,44 @@ +--- +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, search, ast_grep, lsp +--- + +# 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 via the `task` tool. +- If injected workflow-state breadcrumbs 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 and follow the spec and research files listed in the task's `implement.jsonl`. +3. Implement the requested change using existing project patterns. +4. Run the relevant lint, typecheck, and focused tests for the touched code. +5. 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/packages/cli/src/templates/omp/agents/trellis-research.md b/packages/cli/src/templates/omp/agents/trellis-research.md new file mode 100644 index 000000000..372167bd2 --- /dev/null +++ b/packages/cli/src/templates/omp/agents/trellis-research.md @@ -0,0 +1,29 @@ +--- +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, search, web_search +--- + +# 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/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt new file mode 100644 index 000000000..7a5bb1a04 --- /dev/null +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -0,0 +1,292 @@ +import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { spawnSync } from "node:child_process"; + +// --------------------------------------------------------------------------- +// Project root detection +// --------------------------------------------------------------------------- + +function findProjectRoot(startDir: string): string | null { + let current = startDir; + while (true) { + if (existsSync(join(current, ".trellis"))) return current; + const parent = dirname(current); + if (parent === current) break; + current = parent; + } + return null; +} + +// --------------------------------------------------------------------------- +// Active task resolution +// --------------------------------------------------------------------------- + +function resolveActiveTaskStatus( + projectRoot: string, +): { status: string; taskDir: string | null } { + const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); + if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null }; + + let sessionFiles: string[]; + try { + sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); + } catch { + return { status: "no_task", taskDir: null }; + } + if (sessionFiles.length === 0) return { status: "no_task", taskDir: null }; + + if (sessionFiles.length > 1) { + sessionFiles.sort((a, b) => { + const ma = statSync(join(sessionsDir, a)).mtimeMs; + const mb = statSync(join(sessionsDir, b)).mtimeMs; + return mb - ma; + }); + } + + const sessionFile = sessionFiles[0]; + let sessionData: Record<string, unknown>; + try { + sessionData = JSON.parse( + readFileSync(join(sessionsDir, sessionFile), "utf-8"), + ); + } catch { + return { status: "no_task", taskDir: null }; + } + + const currentTask = sessionData.current_task; + if (typeof currentTask !== "string" || !currentTask) + return { status: "no_task", taskDir: null }; + + const taskDir = join(projectRoot, currentTask); + const taskJsonPath = join(taskDir, "task.json"); + if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null }; + + let taskData: Record<string, unknown>; + try { + taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8")); + } catch { + return { status: "no_task", taskDir: null }; + } + + return { + status: typeof taskData.status === "string" ? taskData.status : "planning", + taskDir, + }; +} + +// --------------------------------------------------------------------------- +// Task context — prd.md, info.md, and jsonl-referenced spec/research files +// --------------------------------------------------------------------------- + +function buildTaskContext(projectRoot: string, taskDir: string): string { + const parts: string[] = []; + + let prd = ""; + try { prd = readFileSync(join(taskDir, "prd.md"), "utf-8"); } catch { } + if (prd.trim()) parts.push(`## PRD\n\n${prd.trim()}`); + + let info = ""; + try { info = readFileSync(join(taskDir, "info.md"), "utf-8"); } catch { } + if (info.trim()) parts.push(`## Info\n\n${info.trim()}`); + + for (const jsonlName of ["implement.jsonl", "check.jsonl"]) { + const jsonlPath = join(taskDir, jsonlName); + if (!existsSync(jsonlPath)) continue; + + let lines: string[]; + try { + lines = readFileSync(jsonlPath, "utf-8").split(/\r?\n/); + } catch { + continue; + } + + const fileChunks: string[] = []; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const row = JSON.parse(trimmed) as Record<string, unknown>; + const file = typeof row.file === "string" ? row.file.trim() : ""; + if (!file) continue; + let content = ""; + try { content = readFileSync(join(projectRoot, file), "utf-8"); } catch { } + if (content.trim()) { + fileChunks.push(`### ${file}\n\n${content.trim()}`); + } + } catch { + // seed rows and malformed lines are non-fatal + } + } + + if (fileChunks.length > 0) { + parts.push(`## ${jsonlName}\n\n${fileChunks.join("\n\n---\n\n")}`); + } + } + + return parts.length > 0 + ? `<task-context>\n${parts.join("\n\n")}\n</task-context>` + : ""; +} + +// --------------------------------------------------------------------------- +// Session overview — spawns get_context.py; non-fatal fallback on failure +// --------------------------------------------------------------------------- + +const SESSION_OVERVIEW_TIMEOUT_MS = 5000; +const SESSION_OVERVIEW_FALLBACK = + "Trellis workflow system active. Use skills and agents as directed by the workflow state."; + +function buildSessionOverview(projectRoot: string): string { + const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); + if (!existsSync(script)) return SESSION_OVERVIEW_FALLBACK; + + try { + const result = spawnSync("python3", [script, "--mode", "session-overview"], { + cwd: projectRoot, + encoding: "utf-8", + timeout: SESSION_OVERVIEW_TIMEOUT_MS, + windowsHide: true, + }); + if (result.status !== 0 || !result.stdout?.trim()) { + return SESSION_OVERVIEW_FALLBACK; + } + return result.stdout.trim(); + } catch { + return SESSION_OVERVIEW_FALLBACK; + } +} + +// --------------------------------------------------------------------------- +// Per-turn cache — prevents double-spawn when input + before_agent_start +// fire in the same turn +// --------------------------------------------------------------------------- + +class TurnContextCache { + private key: string | null = null; + private timestamp = 0; + private workflowMsg = ""; + private taskContext = ""; + private static readonly TTL_MS = 1500; + + get(projectRoot: string): { workflowMsg: string; taskContext: string } { + const now = Date.now(); + if ( + this.key === projectRoot && + now - this.timestamp < TurnContextCache.TTL_MS + ) { + return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + } + + const { status, taskDir } = resolveActiveTaskStatus(projectRoot); + + const workflowPath = join(projectRoot, ".trellis", "workflow.md"); + let workflowMd = ""; + try { workflowMd = readFileSync(workflowPath, "utf-8"); } catch { } + + let workflowBody = ""; + if (workflowMd) { + const blocks = parseWorkflowStateBlocks(workflowMd); + const activeBlock = blocks.find((b) => b.status === status); + if (activeBlock) { + workflowBody = `[workflow-state:${activeBlock.status}]\n${activeBlock.content}\n[/workflow-state:${activeBlock.status}]`; + } + } + if (!workflowBody) { + workflowBody = "Refer to workflow.md for current step."; + } + + const overview = buildSessionOverview(projectRoot); + this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${overview}\n</session-overview>`; + this.taskContext = taskDir ? buildTaskContext(projectRoot, taskDir) : ""; + + this.key = projectRoot; + this.timestamp = now; + return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + } +} + +// --------------------------------------------------------------------------- +// Workflow-state tag parsing +// --------------------------------------------------------------------------- + +const WORKFLOW_STATE_RE = + /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g; + +interface WorkflowStateBlock { + status: string; + content: string; +} + +function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] { + const blocks: WorkflowStateBlock[] = []; + for (const match of markdown.matchAll(WORKFLOW_STATE_RE)) { + blocks.push({ + status: match[1], + content: match[2].trim(), + }); + } + return blocks; +} + +// --------------------------------------------------------------------------- +// Extension entry point +// --------------------------------------------------------------------------- + +export default function(pi: ExtensionAPI): void { + pi.setLabel("Trellis"); + + let projectRoot: string | null = null; + const turnCache = new TurnContextCache(); + + pi.on("session_start", async (_event, ctx) => { + projectRoot = findProjectRoot(ctx.cwd); + if (projectRoot) { + ctx.ui.notify("Trellis workflow system available", "info"); + } + }); + + pi.on("before_agent_start", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); + } + if (!projectRoot) return; + + const cached = turnCache.get(projectRoot); + + if (cached.taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: cached.taskContext, + display: false, + }); + } + + return { + message: { + customType: "trellis-workflow-state", + content: cached.workflowMsg, + display: false, + }, + }; + }); + + pi.on("input", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); + } + if (!projectRoot) return { action: "continue" }; + const cached = turnCache.get(projectRoot); + if (cached.workflowMsg) { + return { + action: "continue", + message: { + customType: "trellis-workflow-state", + content: cached.workflowMsg, + display: false, + }, + }; + } + return { action: "continue" }; + }); +} diff --git a/packages/cli/src/templates/omp/index.ts b/packages/cli/src/templates/omp/index.ts new file mode 100644 index 000000000..a5198c14c --- /dev/null +++ b/packages/cli/src/templates/omp/index.ts @@ -0,0 +1,12 @@ +import { createTemplateReader } from "../template-utils.js"; +import type { AgentTemplate } from "../template-utils.js"; + +const { listMdAgents, readTemplate } = createTemplateReader(import.meta.url); + +export function getAllAgents(): AgentTemplate[] { + return listMdAgents(); +} + +export function getExtensionTemplate(): string { + return readTemplate("extensions/trellis/index.ts.txt"); +} diff --git a/packages/cli/src/templates/trellis/scripts/common/cli_adapter.py b/packages/cli/src/templates/trellis/scripts/common/cli_adapter.py index 8d594a56a..e74a799ef 100755 --- a/packages/cli/src/templates/trellis/scripts/common/cli_adapter.py +++ b/packages/cli/src/templates/trellis/scripts/common/cli_adapter.py @@ -55,6 +55,7 @@ "droid", "pi", "trae", + "omp", ] @@ -131,6 +132,8 @@ def config_dir_name(self) -> str: return ".pi" elif self.platform == "trae": return ".trae" + elif self.platform == "omp": + return ".omp" else: return ".claude" @@ -188,6 +191,17 @@ def get_commands_path(self, project_root: Path, *parts: str) -> Path: filename = filename[:-3] return prompts_dir / f"trellis-{filename}.md" return prompts_dir / Path(*parts) + if self.platform == "omp": + commands_dir = self.get_config_dir(project_root) / "commands" + if not parts: + return commands_dir + if len(parts) >= 2 and parts[0] == "trellis": + filename = parts[-1] + if filename.endswith(".md"): + filename = filename[:-3] + return commands_dir / f"trellis-{filename}.md" + return commands_dir / Path(*parts) + if self.platform == "devin": workflow_dir = self.get_config_dir(project_root) / "workflows" @@ -272,6 +286,8 @@ def get_trellis_command_path(self, name: str) -> str: return f".factory/commands/trellis/{name}.md" elif self.platform == "pi": return f".pi/prompts/trellis-{name}.md" + elif self.platform == "omp": + return f".omp/commands/trellis-{name}.md" else: return f"{self.config_dir_name}/commands/trellis/{name}.md" @@ -311,6 +327,8 @@ def get_non_interactive_env(self) -> dict[str, str]: return {} elif self.platform == "trae": return {} + elif self.platform == "omp": + return {} else: return {"CLAUDE_NON_INTERACTIVE": "1"} @@ -400,6 +418,10 @@ def build_run_command( raise ValueError( "Trae is IDE-only; CLI agent run is not supported." ) + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI agent run is not supported." + ) else: # claude cmd = ["claude", "-p"] @@ -470,6 +492,10 @@ def build_resume_command(self, session_id: str) -> list[str]: raise ValueError( "Trae is IDE-only; CLI resume is not supported." ) + elif self.platform == "omp": + raise ValueError( + "OMP uses native task tool for agent runs; CLI resume is not supported." + ) else: return ["claude", "--resume", session_id] @@ -546,6 +572,8 @@ def cli_name(self) -> str: return "pi" elif self.platform == "trae": return "trae" + elif self.platform == "omp": + return "omp" else: return "claude" @@ -642,9 +670,10 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: "droid", "pi", "trae", + "omp", ): 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')" + f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or 'omp')" ) return CLIAdapter(platform=platform) # type: ignore @@ -668,6 +697,7 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter: ".factory", ".pi", ".trae", + ".omp", ) """Platform-specific config directory names used by detect_platform exclusion checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform @@ -737,6 +767,7 @@ def detect_platform(project_root: Path) -> Platform: "droid", "pi", "trae", + "omp", ): return env_platform # type: ignore @@ -817,6 +848,10 @@ def detect_platform(project_root: Path) -> Platform: if (project_root / ".trae").is_dir(): return "trae" + # Check for .omp directory (OMP-specific) + if (project_root / ".omp").is_dir(): + return "omp" + # Fallback: checkout only has the Codex shared-skills layer # (.agents/skills/trellis-* dirs) and no explicit platform config dir. # Happens on fresh clones where .codex/ is gitignored/absent but the diff --git a/packages/cli/src/templates/trellis/scripts/common/task_store.py b/packages/cli/src/templates/trellis/scripts/common/task_store.py index bed8f34fa..a391b6636 100644 --- a/packages/cli/src/templates/trellis/scripts/common/task_store.py +++ b/packages/cli/src/templates/trellis/scripts/common/task_store.py @@ -129,6 +129,7 @@ def _repo_relative_path(path: Path, repo_root: Path) -> str: ".github/copilot", ".pi", # Pi Agent ".trae", # Trae IDE + ".omp", # Oh My Pi ) _SEED_EXAMPLE = ( diff --git a/packages/cli/src/templates/trellis/workflow.md b/packages/cli/src/templates/trellis/workflow.md index 5f703660a..ddba1b4e8 100644 --- a/packages/cli/src/templates/trellis/workflow.md +++ b/packages/cli/src/templates/trellis/workflow.md @@ -183,7 +183,7 @@ Complex task: ask the user if you can create a Trellis task and enter the planni - 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.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My 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 @@ -380,7 +380,7 @@ Brainstorm and research can interleave freely — pause to research a technical #### 1.3 Configure context `[required · once]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My 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. @@ -425,7 +425,7 @@ Ready gate: both `implement.jsonl` and `check.jsonl` must contain at least one r 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] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] [codex-inline, Kilo, Antigravity, Devin] diff --git a/packages/cli/src/types/ai-tools.ts b/packages/cli/src/types/ai-tools.ts index dfe784905..65799d57a 100644 --- a/packages/cli/src/types/ai-tools.ts +++ b/packages/cli/src/types/ai-tools.ts @@ -8,131 +8,134 @@ * Supported AI coding tools */ export type AITool = - | "claude-code" - | "cursor" - | "opencode" - | "codex" - | "kilo" - | "kiro" - | "gemini" - | "antigravity" - | "devin" - | "qoder" - | "codebuddy" - | "copilot" - | "droid" - | "pi" - | "reasonix" - | "zcode" - | "trae"; + | "claude-code" + | "cursor" + | "opencode" + | "codex" + | "kilo" + | "kiro" + | "gemini" + | "antigravity" + | "devin" + | "qoder" + | "codebuddy" + | "copilot" + | "droid" + | "pi" + | "reasonix" + | "zcode" + | "trae" + | "omp"; /** * Template directory categories */ export type TemplateDir = - | "common" - | "claude" - | "cursor" - | "opencode" - | "codex" - | "kilo" - | "kiro" - | "gemini" - | "antigravity" - | "devin" - | "qoder" - | "codebuddy" - | "copilot" - | "droid" - | "pi" - | "reasonix" - | "zcode" - | "trae"; + | "common" + | "claude" + | "cursor" + | "opencode" + | "codex" + | "kilo" + | "kiro" + | "gemini" + | "antigravity" + | "devin" + | "qoder" + | "codebuddy" + | "copilot" + | "droid" + | "pi" + | "reasonix" + | "zcode" + | "trae" + | "omp"; /** * CLI flag names for platform selection (e.g., --claude, --cursor, --kilo, --kiro, --gemini, --antigravity) * Must match keys in InitOptions (src/commands/init.ts) */ export type CliFlag = - | "claude" - | "cursor" - | "opencode" - | "codex" - | "kilo" - | "kiro" - | "gemini" - | "antigravity" - | "devin" - | "qoder" - | "codebuddy" - | "copilot" - | "droid" - | "pi" - | "reasonix" - | "zcode" - | "trae"; + | "claude" + | "cursor" + | "opencode" + | "codex" + | "kilo" + | "kiro" + | "gemini" + | "antigravity" + | "devin" + | "qoder" + | "codebuddy" + | "copilot" + | "droid" + | "pi" + | "reasonix" + | "zcode" + | "trae" + | "omp"; /** * Template context for placeholder resolution. * Controls how common templates are rendered per platform. */ export interface TemplateContext { - /** Prefix for cross-referencing other commands/skills */ - cmdRefPrefix: "/trellis:" | "/trellis-" | "$" | "/" | "/skill trellis-"; - /** Description of AI executor actions shown in role tables */ - executorAI: - | "Bash scripts or Task calls" - | "Bash scripts or tool calls" - | "Bash scripts or Agent calls" - | "Bash scripts or file reads"; - /** Label for user-invocable actions */ - userActionLabel: - | "Slash commands" - | "Skills" - | "Workflows" - | "Prompts" - | "Commands"; - /** Platform supports spawning sub-agents with isolated context */ - agentCapable: boolean; - /** Platform has hook system (SessionStart, PreToolUse) */ - hasHooks: boolean; - /** - * CLI flag value for this platform (e.g. "claude", "codex", "kiro"). - * Substituted into template commands via {{CLI_FLAG}} so rendered skill / - * command files can pass `--platform <flag>` to scripts that need to know - * the invoking platform, removing the need to re-detect at runtime. - * Duplicates the top-level `AIToolConfig.cliFlag` for convenience — the - * invariant is maintained in `AI_TOOLS` config blocks. - */ - cliFlag: CliFlag; + /** Prefix for cross-referencing other commands/skills */ + cmdRefPrefix: "/trellis:" | "/trellis-" | "$" | "/" | "/skill trellis-"; + /** Description of AI executor actions shown in role tables */ + executorAI: + | "Bash scripts or Task calls" + | "Bash scripts or tool calls" + | "Bash scripts or Agent calls" + | "Bash scripts or file reads"; + /** Label for user-invocable actions */ + userActionLabel: + | "Slash commands" + | "Skills" + | "Workflows" + | "Prompts" + | "Commands"; + /** Platform supports spawning sub-agents with isolated context */ + agentCapable: boolean; + /** Platform has hook system (SessionStart, PreToolUse) */ + hasHooks: boolean; + /** + * CLI flag value for this platform (e.g. "claude", "codex", "kiro"). + * Substituted into template commands via {{CLI_FLAG}} so rendered skill / + * command files can pass `--platform <flag>` to scripts that need to know + * the invoking platform, removing the need to re-detect at runtime. + * Duplicates the top-level `AIToolConfig.cliFlag` for convenience — the + * invariant is maintained in `AI_TOOLS` config blocks. + */ + cliFlag: CliFlag; } /** * Configuration for an AI tool */ export interface AIToolConfig { - /** Display name of the tool */ - name: string; - /** Command template directory names to include */ - templateDirs: TemplateDir[]; - /** Config directory name in the project root (e.g., ".claude") */ - configDir: string; - /** - * Whether the platform supports the shared `.agents/skills/` layer - * (agentskills.io open standard). When true, `.agents/skills` is added - * to the platform's managed paths automatically. - */ - supportsAgentSkills?: boolean; - /** Additional managed paths beyond configDir (e.g., .github/hooks for Copilot) */ - extraManagedPaths?: string[]; - /** CLI flag name for --flag options (e.g., "claude" for --claude) */ - cliFlag: CliFlag; - /** Whether this tool is checked by default in interactive init prompt */ - defaultChecked: boolean; - /** Whether this tool uses Python hooks (affects Windows encoding detection) */ - hasPythonHooks: boolean; - /** Template context for placeholder resolution in common templates */ - templateContext: TemplateContext; + /** Display name of the tool */ + name: string; + /** Command template directory names to include */ + templateDirs: TemplateDir[]; + /** Config directory name in the project root (e.g., ".claude") */ + configDir: string; + /** + * Whether the platform supports the shared `.agents/skills/` layer + * (agentskills.io open standard). When true, `.agents/skills` is added + * to the platform's managed paths automatically. + */ + supportsAgentSkills?: boolean; + /** Additional managed paths beyond configDir (e.g., .github/hooks for Copilot) */ + extraManagedPaths?: string[]; + /** CLI flag name for --flag options (e.g., "claude" for --claude) */ + cliFlag: CliFlag; + /** Whether this tool is checked by default in interactive init prompt */ + defaultChecked: boolean; + /** Whether this tool uses Python hooks (affects Windows encoding detection) */ + hasPythonHooks: boolean; + /** Template context for placeholder resolution in common templates */ + templateContext: TemplateContext; } /** @@ -147,318 +150,334 @@ export interface AIToolConfig { * 5. Add to InitOptions in src/commands/init.ts */ export const AI_TOOLS: Record<AITool, AIToolConfig> = { - "claude-code": { - name: "Claude Code", - templateDirs: ["common", "claude"], - configDir: ".claude", - cliFlag: "claude", - defaultChecked: true, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "claude", - }, + "claude-code": { + name: "Claude Code", + templateDirs: ["common", "claude"], + configDir: ".claude", + cliFlag: "claude", + defaultChecked: true, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "claude", }, - cursor: { - name: "Cursor", - templateDirs: ["common", "cursor"], - configDir: ".cursor", - cliFlag: "cursor", - defaultChecked: true, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis-", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "cursor", - }, + }, + cursor: { + name: "Cursor", + templateDirs: ["common", "cursor"], + configDir: ".cursor", + cliFlag: "cursor", + defaultChecked: true, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis-", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "cursor", }, - opencode: { - name: "OpenCode", - templateDirs: ["common", "opencode"], - configDir: ".opencode", - cliFlag: "opencode", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: false, - cliFlag: "opencode", - }, + }, + opencode: { + name: "OpenCode", + templateDirs: ["common", "opencode"], + configDir: ".opencode", + cliFlag: "opencode", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: false, + cliFlag: "opencode", }, - codex: { - name: "Codex (also writes .agents/skills/ — read by Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code)", - templateDirs: ["common", "codex"], - configDir: ".codex", - supportsAgentSkills: true, - cliFlag: "codex", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "$", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Skills", - agentCapable: true, - hasHooks: false, - cliFlag: "codex", - }, + }, + codex: { + name: "Codex (also writes .agents/skills/ — read by Cursor, Gemini CLI, GitHub Copilot, Amp, Kimi Code)", + templateDirs: ["common", "codex"], + configDir: ".codex", + supportsAgentSkills: true, + cliFlag: "codex", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "$", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Skills", + agentCapable: true, + hasHooks: false, + cliFlag: "codex", }, - kilo: { - name: "Kilo CLI", - templateDirs: ["common", "kilo"], - configDir: ".kilocode", - cliFlag: "kilo", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or file reads", - userActionLabel: "Workflows", - agentCapable: false, - hasHooks: false, - cliFlag: "kilo", - }, + }, + kilo: { + name: "Kilo CLI", + templateDirs: ["common", "kilo"], + configDir: ".kilocode", + cliFlag: "kilo", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or file reads", + userActionLabel: "Workflows", + agentCapable: false, + hasHooks: false, + cliFlag: "kilo", }, - kiro: { - name: "Kiro Code", - templateDirs: ["common", "kiro"], - configDir: ".kiro/skills", - extraManagedPaths: [".kiro/agents", ".kiro/hooks"], - cliFlag: "kiro", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "$", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Skills", - agentCapable: true, - hasHooks: true, - cliFlag: "kiro", - }, + }, + kiro: { + name: "Kiro Code", + templateDirs: ["common", "kiro"], + configDir: ".kiro/skills", + extraManagedPaths: [".kiro/agents", ".kiro/hooks"], + cliFlag: "kiro", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "$", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Skills", + agentCapable: true, + hasHooks: true, + cliFlag: "kiro", }, - gemini: { - name: "Gemini CLI", - templateDirs: ["common", "gemini"], - configDir: ".gemini", - supportsAgentSkills: true, - cliFlag: "gemini", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "gemini", - }, + }, + gemini: { + name: "Gemini CLI", + templateDirs: ["common", "gemini"], + configDir: ".gemini", + supportsAgentSkills: true, + cliFlag: "gemini", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "gemini", }, - antigravity: { - name: "Antigravity", - templateDirs: ["common", "antigravity"], - configDir: ".agent/workflows", - extraManagedPaths: [".agent/skills"], - cliFlag: "antigravity", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/", - executorAI: "Bash scripts or file reads", - userActionLabel: "Workflows", - agentCapable: false, - hasHooks: false, - cliFlag: "antigravity", - }, + }, + antigravity: { + name: "Antigravity", + templateDirs: ["common", "antigravity"], + configDir: ".agent/workflows", + extraManagedPaths: [".agent/skills"], + cliFlag: "antigravity", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/", + executorAI: "Bash scripts or file reads", + userActionLabel: "Workflows", + agentCapable: false, + hasHooks: false, + cliFlag: "antigravity", }, - devin: { - name: "Devin", - templateDirs: ["common", "devin"], - configDir: ".devin/workflows", - extraManagedPaths: [".devin/skills"], - cliFlag: "devin", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis-", - executorAI: "Bash scripts or file reads", - userActionLabel: "Workflows", - agentCapable: false, - hasHooks: false, - cliFlag: "devin", - }, + }, + devin: { + name: "Devin", + templateDirs: ["common", "devin"], + configDir: ".devin/workflows", + extraManagedPaths: [".devin/skills"], + cliFlag: "devin", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis-", + executorAI: "Bash scripts or file reads", + userActionLabel: "Workflows", + agentCapable: false, + hasHooks: false, + cliFlag: "devin", }, - qoder: { - name: "Qoder", - templateDirs: ["common", "qoder"], - configDir: ".qoder", - cliFlag: "qoder", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "$", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Skills", - agentCapable: true, - hasHooks: true, - cliFlag: "qoder", - }, + }, + qoder: { + name: "Qoder", + templateDirs: ["common", "qoder"], + configDir: ".qoder", + cliFlag: "qoder", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "$", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Skills", + agentCapable: true, + hasHooks: true, + cliFlag: "qoder", }, - codebuddy: { - name: "CodeBuddy", - templateDirs: ["common", "codebuddy"], - configDir: ".codebuddy", - cliFlag: "codebuddy", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "codebuddy", - }, + }, + codebuddy: { + name: "CodeBuddy", + templateDirs: ["common", "codebuddy"], + configDir: ".codebuddy", + cliFlag: "codebuddy", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "codebuddy", }, - copilot: { - name: "GitHub Copilot", - templateDirs: ["common", "copilot"], - configDir: ".github/copilot", - extraManagedPaths: [ - ".github/agents", - ".github/hooks", - ".github/prompts", - ".github/skills", - ], - cliFlag: "copilot", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Prompts", - agentCapable: true, - hasHooks: true, - cliFlag: "copilot", - }, + }, + copilot: { + name: "GitHub Copilot", + templateDirs: ["common", "copilot"], + configDir: ".github/copilot", + extraManagedPaths: [ + ".github/agents", + ".github/hooks", + ".github/prompts", + ".github/skills", + ], + cliFlag: "copilot", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Prompts", + agentCapable: true, + hasHooks: true, + cliFlag: "copilot", }, - droid: { - name: "Factory Droid", - templateDirs: ["common", "droid"], - configDir: ".factory", - cliFlag: "droid", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis-", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "droid", - }, + }, + droid: { + name: "Factory Droid", + templateDirs: ["common", "droid"], + configDir: ".factory", + cliFlag: "droid", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis-", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "droid", }, - pi: { - name: "Pi Agent", - templateDirs: ["common", "pi"], - configDir: ".pi", - cliFlag: "pi", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis-", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Slash commands", - agentCapable: true, - hasHooks: true, - cliFlag: "pi", - }, + }, + pi: { + name: "Pi Agent", + templateDirs: ["common", "pi"], + configDir: ".pi", + cliFlag: "pi", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis-", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "pi", }, - reasonix: { - name: "Reasonix", - templateDirs: ["common", "reasonix"], - configDir: ".reasonix", - cliFlag: "reasonix", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/skill trellis-", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Skills", - agentCapable: true, - hasHooks: false, - cliFlag: "reasonix", - }, + }, + reasonix: { + name: "Reasonix", + templateDirs: ["common", "reasonix"], + configDir: ".reasonix", + cliFlag: "reasonix", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/skill trellis-", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Skills", + agentCapable: true, + hasHooks: false, + cliFlag: "reasonix", }, - zcode: { - name: "ZCode", - templateDirs: ["common", "zcode"], - configDir: ".zcode", - supportsAgentSkills: true, - extraManagedPaths: [".zcode/cli/agents", ".zcode/commands"], - cliFlag: "zcode", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or Agent calls", - userActionLabel: "Skills", - agentCapable: true, - hasHooks: false, - cliFlag: "zcode", - }, + }, + zcode: { + name: "ZCode", + templateDirs: ["common", "zcode"], + configDir: ".zcode", + supportsAgentSkills: true, + extraManagedPaths: [".zcode/cli/agents", ".zcode/commands"], + cliFlag: "zcode", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Agent calls", + userActionLabel: "Skills", + agentCapable: true, + hasHooks: false, + cliFlag: "zcode", }, - trae: { - name: "Trae", - templateDirs: ["common", "trae"], - configDir: ".trae", - cliFlag: "trae", - defaultChecked: false, - hasPythonHooks: true, - templateContext: { - cmdRefPrefix: "/trellis-", - executorAI: "Bash scripts or tool calls", - userActionLabel: "Commands", - agentCapable: true, - hasHooks: true, - cliFlag: "trae", - }, + }, + trae: { + name: "Trae", + templateDirs: ["common", "trae"], + configDir: ".trae", + cliFlag: "trae", + defaultChecked: false, + hasPythonHooks: true, + templateContext: { + cmdRefPrefix: "/trellis-", + executorAI: "Bash scripts or tool calls", + userActionLabel: "Commands", + agentCapable: true, + hasHooks: true, + cliFlag: "trae", }, + }, + omp: { + name: "Oh My Pi", + templateDirs: ["common", "omp"], + configDir: ".omp", + cliFlag: "omp", + defaultChecked: false, + hasPythonHooks: false, + templateContext: { + cmdRefPrefix: "/trellis:", + executorAI: "Bash scripts or Task calls", + userActionLabel: "Slash commands", + agentCapable: true, + hasHooks: true, + cliFlag: "omp", + }, + }, }; /** * Get the configuration for a specific AI tool */ export function getToolConfig(tool: AITool): AIToolConfig { - return AI_TOOLS[tool]; + return AI_TOOLS[tool]; } /** * Get all managed paths for a specific tool. */ export function getManagedPaths(tool: AITool): string[] { - const config = AI_TOOLS[tool]; - const paths = [config.configDir]; - if (config.supportsAgentSkills) { - paths.push(".agents/skills"); - } - if (config.extraManagedPaths) { - paths.push(...config.extraManagedPaths); - } - return paths; + const config = AI_TOOLS[tool]; + const paths = [config.configDir]; + if (config.supportsAgentSkills) { + paths.push(".agents/skills"); + } + if (config.extraManagedPaths) { + paths.push(...config.extraManagedPaths); + } + return paths; } /** * Get template directories for a specific tool */ export function getTemplateDirs(tool: AITool): TemplateDir[] { - return AI_TOOLS[tool].templateDirs; + return AI_TOOLS[tool].templateDirs; } diff --git a/packages/cli/test/regression.test.ts b/packages/cli/test/regression.test.ts index ae38605e5..266733065 100644 --- a/packages/cli/test/regression.test.ts +++ b/packages/cli/test/regression.test.ts @@ -738,6 +738,7 @@ describe("regression: update only configured platforms (beta.16)", () => { "copilot", "droid", "pi", + "omp", ] as const; for (const id of withTracking) { const result = collectPlatformTemplates(id); @@ -4454,6 +4455,15 @@ describe("regression: platform additions (beta.9, beta.13, beta.16)", () => { expect(AI_TOOLS.pi.templateContext.hasHooks).toBe(true); }); + it("[omp] Oh My Pi platform is registered", () => { + expect(AI_TOOLS).toHaveProperty("omp"); + expect(AI_TOOLS.omp.configDir).toBe(".omp"); + expect(AI_TOOLS.omp.cliFlag).toBe("omp"); + expect(AI_TOOLS.omp.hasPythonHooks).toBe(false); + expect(AI_TOOLS.omp.templateContext.agentCapable).toBe(true); + expect(AI_TOOLS.omp.templateContext.hasHooks).toBe(true); + }); + it("[beta.9] all platforms have consistent required fields", () => { for (const id of PLATFORM_IDS) { const tool = AI_TOOLS[id]; @@ -4537,6 +4547,11 @@ describe("regression: cli_adapter platform support (beta.9, beta.13, beta.16)", ); }); + it("[omp] cli_adapter.py supports omp platform", () => { + expect(commonCliAdapter).toContain('"omp"'); + expect(commonCliAdapter).toContain(".omp"); + }); + it("[droid] cli_adapter.py treats droid as commands-only (no CLI run/resume yet)", () => { expect(commonCliAdapter).toContain( "Factory Droid CLI agent run is not yet supported.", @@ -4782,6 +4797,7 @@ describe("regression: cli_adapter platform support (beta.9, beta.13, beta.16)", expect(commonCliAdapter).toContain(".github/copilot"); expect(commonCliAdapter).toContain(".factory"); expect(commonCliAdapter).toContain(".pi"); + expect(commonCliAdapter).toContain(".omp"); }); it("[copilot] cli_adapter.py treats copilot as IDE-only (no CLI run/resume)", () => { diff --git a/packages/cli/test/templates/omp.test.ts b/packages/cli/test/templates/omp.test.ts new file mode 100644 index 000000000..88ef587e7 --- /dev/null +++ b/packages/cli/test/templates/omp.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + getAllAgents, + getExtensionTemplate, +} from "../../src/templates/omp/index.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const templateDir = path.resolve(__dirname, "../../src/templates/omp"); + +describe("omp templates", () => { + it("provides the three Trellis sub-agent definitions", () => { + const agents = getAllAgents(); + expect(agents.map((agent) => agent.name).sort()).toEqual([ + "trellis-check", + "trellis-implement", + "trellis-research", + ]); + }); + + it("each agent has non-empty content and name", () => { + for (const agent of getAllAgents()) { + expect(agent.name.length).toBeGreaterThan(0); + expect(agent.content.length).toBeGreaterThan(0); + } + }); + + it("getExtensionTemplate returns a non-empty string", () => { + const extension = getExtensionTemplate(); + expect(extension.length).toBeGreaterThan(0); + }); + + it("extension template contains key markers for OMP integration", () => { + const extension = getExtensionTemplate(); + expect(extension).toContain("before_agent_start"); + expect(extension).toContain("input"); + expect(extension).toContain("session_start"); + expect(extension).toContain("ExtensionAPI"); + }); + + it("no settings.json or Python hooks exist in the template directory", () => { + // OMP is extension-backed: native provider auto-discovers .omp/ subdirs, + // so no settings.json is needed and no Python hooks should be present. + expect(fs.existsSync(path.join(templateDir, "settings.json"))).toBe(false); + expect(fs.existsSync(path.join(templateDir, "hooks"))).toBe(false); + + // Agents must not reference Python hook scripts + for (const agent of getAllAgents()) { + expect(agent.content).not.toContain("inject-subagent-context.py"); + } + }); +}); From 5b08762e510edb19332a516c2f7cc6ad6c5400cd Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Mon, 25 May 2026 14:49:19 +0800 Subject: [PATCH 02/19] chore(task): archive 05-22-add-omp-platform-support-to-trellis --- .../05-22-add-omp-platform-support-to-trellis/check.jsonl | 0 .../05-22-add-omp-platform-support-to-trellis/implement.jsonl | 0 .../2026-05}/05-22-add-omp-platform-support-to-trellis/prd.md | 0 .../05-22-add-omp-platform-support-to-trellis/task.json | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename .trellis/tasks/{ => archive/2026-05}/05-22-add-omp-platform-support-to-trellis/check.jsonl (100%) rename .trellis/tasks/{ => archive/2026-05}/05-22-add-omp-platform-support-to-trellis/implement.jsonl (100%) rename .trellis/tasks/{ => archive/2026-05}/05-22-add-omp-platform-support-to-trellis/prd.md (100%) rename .trellis/tasks/{ => archive/2026-05}/05-22-add-omp-platform-support-to-trellis/task.json (90%) diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl similarity index 100% rename from .trellis/tasks/05-22-add-omp-platform-support-to-trellis/check.jsonl rename to .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl similarity index 100% rename from .trellis/tasks/05-22-add-omp-platform-support-to-trellis/implement.jsonl rename to .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md similarity index 100% rename from .trellis/tasks/05-22-add-omp-platform-support-to-trellis/prd.md rename to .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md diff --git a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json similarity index 90% rename from .trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json rename to .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json index 19da5cba8..530376c32 100644 --- a/.trellis/tasks/05-22-add-omp-platform-support-to-trellis/task.json +++ b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json @@ -3,7 +3,7 @@ "name": "add-omp-platform-support-to-trellis", "title": "Add OMP platform support to Trellis", "description": "", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": null, "package": null, @@ -11,7 +11,7 @@ "creator": "--name", "assignee": "--name", "createdAt": "2026-05-22", - "completedAt": null, + "completedAt": "2026-05-25", "branch": null, "base_branch": "main", "worktree_path": null, From d1d91b3c81e2df67700d0d481236dd7690163adb Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Mon, 25 May 2026 14:50:53 +0800 Subject: [PATCH 03/19] chore: record journal --- .trellis/workspace/VinciWu557/index.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .trellis/workspace/VinciWu557/index.md diff --git a/.trellis/workspace/VinciWu557/index.md b/.trellis/workspace/VinciWu557/index.md new file mode 100644 index 000000000..d47476207 --- /dev/null +++ b/.trellis/workspace/VinciWu557/index.md @@ -0,0 +1,6 @@ +# Developer Journal — VinciWu557 + +<!-- @@@auto:current-status --> +- **Active File**: `journal-0.md` +- **Total Sessions**: 1 +- **Last Active**: 2026-05-25 \ No newline at end of file From 2cbf9c56378bc2af2e3edfd6ef06edc84503c0cc Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Mon, 25 May 2026 14:58:18 +0800 Subject: [PATCH 04/19] chore: record journal --- .trellis/workspace/VinciWu557/index.md | 4 +- .trellis/workspace/VinciWu557/journal-1.md | 73 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .trellis/workspace/VinciWu557/journal-1.md diff --git a/.trellis/workspace/VinciWu557/index.md b/.trellis/workspace/VinciWu557/index.md index d47476207..399f9178b 100644 --- a/.trellis/workspace/VinciWu557/index.md +++ b/.trellis/workspace/VinciWu557/index.md @@ -1,6 +1,6 @@ # Developer Journal — VinciWu557 <!-- @@@auto:current-status --> -- **Active File**: `journal-0.md` -- **Total Sessions**: 1 +- **Active File**: `journal-1.md` +- **Total Sessions**: 2 - **Last Active**: 2026-05-25 \ No newline at end of file diff --git a/.trellis/workspace/VinciWu557/journal-1.md b/.trellis/workspace/VinciWu557/journal-1.md new file mode 100644 index 000000000..6343c998e --- /dev/null +++ b/.trellis/workspace/VinciWu557/journal-1.md @@ -0,0 +1,73 @@ +# Journal - VinciWu557 (Part 1) + +> Started: 2026-05-25 + +--- + + + +## Session 1: Add Oh My Pi platform support + +**Date**: 2026-05-25 +**Task**: Add Oh My Pi platform support +**Branch**: `feat/support-oh-my-pi` + +### Summary + +Added Oh My Pi (omp) platform support to Trellis, implementing Phase 1 and Phase 2 of the integration. + +### Main Changes + +- Implemented omp platform adapter (Phase 1 + Phase 2) +- Added platform detection and configuration for Oh My Pi + +### Git Commits + +| Hash | Message | +|------|---------| +| `5fb8a25` | feat(omp): add Oh My Pi platform support (Phase 1 + Phase 2) | + +### Testing + +- [OK] Build passes + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete + + +## Session 2: Add Oh My Pi platform support + +**Date**: 2026-05-25 +**Task**: Add Oh My Pi platform support +**Branch**: `feat/support-oh-my-pi` + +### Summary + +Added Oh My Pi (omp) platform support to Trellis, implementing Phase 1 + Phase 2 platform adapter and configuration. + +### Main Changes + +(Add details) + +### Git Commits + +| Hash | Message | +|------|---------| +| `5fb8a25` | (see git log) | + +### Testing + +- [OK] (Add test results) + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete From 08c3ebc7650b5a212764e812b3c9853edcd11532 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Mon, 25 May 2026 17:08:52 +0800 Subject: [PATCH 05/19] feat(oh-my-pi): update workflow documentation template to include Oh My Pi in skill lists --- .../cli/src/templates/trellis/workflow.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/templates/trellis/workflow.md b/packages/cli/src/templates/trellis/workflow.md index ddba1b4e8..7775a9313 100644 --- a/packages/cli/src/templates/trellis/workflow.md +++ b/packages/cli/src/templates/trellis/workflow.md @@ -272,13 +272,13 @@ Code committed. Run `/trellis:finish-work`; if dirty, return to Phase 3.4 first. 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] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My 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] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] [codex-inline, Kilo, Antigravity, Devin] @@ -353,7 +353,7 @@ Return to this step whenever requirements change and revise the relevant artifac 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] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] Spawn the research sub-agent: @@ -361,7 +361,7 @@ Spawn the research sub-agent: - **Task description**: Research <specific question> - **Key requirement**: Research output MUST be persisted to `{TASK_DIR}/research/` -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] [codex-inline, Kilo, Antigravity, Devin] @@ -458,11 +458,11 @@ If `task.py start` errors with a session-identity message (no context key from h | `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] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My 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] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] --- @@ -472,7 +472,7 @@ Goal: turn reviewed planning artifacts into code that passes quality checks. #### 2.1 Implement `[required · repeatable]` -[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi] +[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi, Oh My Pi] Spawn the implement sub-agent: @@ -484,7 +484,7 @@ 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] +[/Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi, Oh My Pi] [codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae] @@ -526,7 +526,7 @@ The platform prelude auto-handles the context load requirement: #### 2.2 Quality check `[required · repeatable]` -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, ZCode, Reasonix, Trae] +[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] Spawn the check sub-agent: @@ -540,7 +540,7 @@ The check agent's job: - 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] +[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi, Oh My Pi, ZCode, Reasonix, Trae] [codex-inline, Kilo, Antigravity, Devin] From 70693fb07c5ed45836b2d345a872a956d735e60d Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Tue, 26 May 2026 20:00:59 +0800 Subject: [PATCH 06/19] feat(omp): add session-start rich injection and sub-agent precision context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - session_start: invoke get_context.py (default mode) for full project map (git/spec/task/developer/journal), matching Claude hook behavior - session_start (sub-agent): inject only agent-relevant jsonl files via PI_BLOCKED_AGENT detection (implement→implement.jsonl, check→check.jsonl, research→prd+info only) - before_agent_start: simplified to workflow-state-only (lightweight per-turn) - Remove dead buildSessionOverview() that called non-existent script mode - Update omp.test.ts with new feature marker assertions --- .omp/extensions/trellis/index.ts | 170 ++++++++++++------ .../omp/extensions/trellis/index.ts.txt | 170 ++++++++++++------ packages/cli/test/templates/omp.test.ts | 106 ++++++----- 3 files changed, 294 insertions(+), 152 deletions(-) diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts index 7a5bb1a04..4bf60e733 100644 --- a/.omp/extensions/trellis/index.ts +++ b/.omp/extensions/trellis/index.ts @@ -24,17 +24,17 @@ function findProjectRoot(startDir: string): string | null { function resolveActiveTaskStatus( projectRoot: string, -): { status: string; taskDir: string | null } { +): { status: string; taskDir: string | null; taskTitle: string | null } { const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); - if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null }; + if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; let sessionFiles: string[]; try { sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } - if (sessionFiles.length === 0) return { status: "no_task", taskDir: null }; + if (sessionFiles.length === 0) return { status: "no_task", taskDir: null, taskTitle: null }; if (sessionFiles.length > 1) { sessionFiles.sort((a, b) => { @@ -51,37 +51,67 @@ function resolveActiveTaskStatus( readFileSync(join(sessionsDir, sessionFile), "utf-8"), ); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } const currentTask = sessionData.current_task; if (typeof currentTask !== "string" || !currentTask) - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; const taskDir = join(projectRoot, currentTask); const taskJsonPath = join(taskDir, "task.json"); - if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null }; + if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null, taskTitle: null }; let taskData: Record<string, unknown>; try { taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8")); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } return { status: typeof taskData.status === "string" ? taskData.status : "planning", taskDir, + taskTitle: typeof taskData.title === "string" ? taskData.title : null, }; } +// --------------------------------------------------------------------------- +// Session context — spawns get_context.py default mode (same as Claude hook) +// --------------------------------------------------------------------------- + +const SESSION_CONTEXT_TIMEOUT_MS = 5000; + +function buildSessionContext(projectRoot: string): string { + const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); + if (!existsSync(script)) return ""; + + try { + const result = spawnSync("python3", [script], { + cwd: projectRoot, + encoding: "utf-8", + timeout: SESSION_CONTEXT_TIMEOUT_MS, + windowsHide: true, + }); + if (result.status !== 0 || !result.stdout?.trim()) { + return ""; + } + return `<session-context>\n${result.stdout.trim()}\n</session-context>`; + } catch { + return ""; + } +} + // --------------------------------------------------------------------------- // Task context — prd.md, info.md, and jsonl-referenced spec/research files // --------------------------------------------------------------------------- -function buildTaskContext(projectRoot: string, taskDir: string): string { +type AgentType = "trellis-implement" | "trellis-check" | "trellis-research" | null; + +function buildTaskContext(projectRoot: string, taskDir: string, agentType?: AgentType): string { const parts: string[] = []; + // prd.md and info.md — always included let prd = ""; try { prd = readFileSync(join(taskDir, "prd.md"), "utf-8"); } catch { } if (prd.trim()) parts.push(`## PRD\n\n${prd.trim()}`); @@ -90,7 +120,19 @@ function buildTaskContext(projectRoot: string, taskDir: string): string { try { info = readFileSync(join(taskDir, "info.md"), "utf-8"); } catch { } if (info.trim()) parts.push(`## Info\n\n${info.trim()}`); - for (const jsonlName of ["implement.jsonl", "check.jsonl"]) { + // Determine which jsonl files to read based on agent type + let jsonlNames: string[]; + if (agentType === "trellis-implement") { + jsonlNames = ["implement.jsonl"]; + } else if (agentType === "trellis-check") { + jsonlNames = ["check.jsonl"]; + } else if (agentType === "trellis-research") { + jsonlNames = []; // research agent gets only prd + info + } else { + jsonlNames = ["implement.jsonl", "check.jsonl"]; // main session: all + } + + for (const jsonlName of jsonlNames) { const jsonlPath = join(taskDir, jsonlName); if (!existsSync(jsonlPath)) continue; @@ -129,56 +171,30 @@ function buildTaskContext(projectRoot: string, taskDir: string): string { : ""; } -// --------------------------------------------------------------------------- -// Session overview — spawns get_context.py; non-fatal fallback on failure -// --------------------------------------------------------------------------- - -const SESSION_OVERVIEW_TIMEOUT_MS = 5000; -const SESSION_OVERVIEW_FALLBACK = - "Trellis workflow system active. Use skills and agents as directed by the workflow state."; - -function buildSessionOverview(projectRoot: string): string { - const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); - if (!existsSync(script)) return SESSION_OVERVIEW_FALLBACK; - - try { - const result = spawnSync("python3", [script, "--mode", "session-overview"], { - cwd: projectRoot, - encoding: "utf-8", - timeout: SESSION_OVERVIEW_TIMEOUT_MS, - windowsHide: true, - }); - if (result.status !== 0 || !result.stdout?.trim()) { - return SESSION_OVERVIEW_FALLBACK; - } - return result.stdout.trim(); - } catch { - return SESSION_OVERVIEW_FALLBACK; - } -} - // --------------------------------------------------------------------------- // Per-turn cache — prevents double-spawn when input + before_agent_start // fire in the same turn // --------------------------------------------------------------------------- +const SESSION_OVERVIEW_TEXT = + "Trellis workflow system active. Use skills and agents as directed by the workflow state."; + class TurnContextCache { private key: string | null = null; private timestamp = 0; private workflowMsg = ""; - private taskContext = ""; private static readonly TTL_MS = 1500; - get(projectRoot: string): { workflowMsg: string; taskContext: string } { + get(projectRoot: string): { workflowMsg: string } { const now = Date.now(); if ( this.key === projectRoot && now - this.timestamp < TurnContextCache.TTL_MS ) { - return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + return { workflowMsg: this.workflowMsg }; } - const { status, taskDir } = resolveActiveTaskStatus(projectRoot); + const { status } = resolveActiveTaskStatus(projectRoot); const workflowPath = join(projectRoot, ".trellis", "workflow.md"); let workflowMd = ""; @@ -196,13 +212,11 @@ class TurnContextCache { workflowBody = "Refer to workflow.md for current step."; } - const overview = buildSessionOverview(projectRoot); - this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${overview}\n</session-overview>`; - this.taskContext = taskDir ? buildTaskContext(projectRoot, taskDir) : ""; + this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`; this.key = projectRoot; this.timestamp = now; - return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + return { workflowMsg: this.workflowMsg }; } } @@ -229,6 +243,20 @@ function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] { return blocks; } +// --------------------------------------------------------------------------- +// Sub-agent detection +// --------------------------------------------------------------------------- + +const TRELLIS_AGENTS = new Set(["trellis-implement", "trellis-check", "trellis-research"]); + +function detectAgentType(): AgentType { + const blocked = process.env.PI_BLOCKED_AGENT; + if (blocked && TRELLIS_AGENTS.has(blocked)) { + return blocked as AgentType; + } + return null; +} + // --------------------------------------------------------------------------- // Extension entry point // --------------------------------------------------------------------------- @@ -238,10 +266,49 @@ export default function(pi: ExtensionAPI): void { let projectRoot: string | null = null; const turnCache = new TurnContextCache(); + const agentType = detectAgentType(); + const isSubAgent = agentType !== null; pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); - if (projectRoot) { + if (!projectRoot) return; + + if (isSubAgent) { + // Sub-agent: inject precise task context once + const { taskDir } = resolveActiveTaskStatus(projectRoot); + if (taskDir) { + const taskContext = buildTaskContext(projectRoot, taskDir, agentType); + if (taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: taskContext, + display: false, + }); + } + } + } else { + // Main session: inject session context (global map) + task context + const sessionContext = buildSessionContext(projectRoot); + if (sessionContext) { + await pi.sendMessage({ + customType: "trellis-session-context", + content: sessionContext, + display: false, + }); + } + + const { taskDir } = resolveActiveTaskStatus(projectRoot); + if (taskDir) { + const taskContext = buildTaskContext(projectRoot, taskDir); + if (taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: taskContext, + display: false, + }); + } + } + ctx.ui.notify("Trellis workflow system available", "info"); } }); @@ -252,16 +319,9 @@ export default function(pi: ExtensionAPI): void { } if (!projectRoot) return; + // Lightweight: workflow state only (no task context — already injected in session_start) const cached = turnCache.get(projectRoot); - if (cached.taskContext) { - await pi.sendMessage({ - customType: "trellis-task-context", - content: cached.taskContext, - display: false, - }); - } - return { message: { customType: "trellis-workflow-state", diff --git a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt index 7a5bb1a04..4bf60e733 100644 --- a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -24,17 +24,17 @@ function findProjectRoot(startDir: string): string | null { function resolveActiveTaskStatus( projectRoot: string, -): { status: string; taskDir: string | null } { +): { status: string; taskDir: string | null; taskTitle: string | null } { const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); - if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null }; + if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; let sessionFiles: string[]; try { sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } - if (sessionFiles.length === 0) return { status: "no_task", taskDir: null }; + if (sessionFiles.length === 0) return { status: "no_task", taskDir: null, taskTitle: null }; if (sessionFiles.length > 1) { sessionFiles.sort((a, b) => { @@ -51,37 +51,67 @@ function resolveActiveTaskStatus( readFileSync(join(sessionsDir, sessionFile), "utf-8"), ); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } const currentTask = sessionData.current_task; if (typeof currentTask !== "string" || !currentTask) - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; const taskDir = join(projectRoot, currentTask); const taskJsonPath = join(taskDir, "task.json"); - if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null }; + if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null, taskTitle: null }; let taskData: Record<string, unknown>; try { taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8")); } catch { - return { status: "no_task", taskDir: null }; + return { status: "no_task", taskDir: null, taskTitle: null }; } return { status: typeof taskData.status === "string" ? taskData.status : "planning", taskDir, + taskTitle: typeof taskData.title === "string" ? taskData.title : null, }; } +// --------------------------------------------------------------------------- +// Session context — spawns get_context.py default mode (same as Claude hook) +// --------------------------------------------------------------------------- + +const SESSION_CONTEXT_TIMEOUT_MS = 5000; + +function buildSessionContext(projectRoot: string): string { + const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); + if (!existsSync(script)) return ""; + + try { + const result = spawnSync("python3", [script], { + cwd: projectRoot, + encoding: "utf-8", + timeout: SESSION_CONTEXT_TIMEOUT_MS, + windowsHide: true, + }); + if (result.status !== 0 || !result.stdout?.trim()) { + return ""; + } + return `<session-context>\n${result.stdout.trim()}\n</session-context>`; + } catch { + return ""; + } +} + // --------------------------------------------------------------------------- // Task context — prd.md, info.md, and jsonl-referenced spec/research files // --------------------------------------------------------------------------- -function buildTaskContext(projectRoot: string, taskDir: string): string { +type AgentType = "trellis-implement" | "trellis-check" | "trellis-research" | null; + +function buildTaskContext(projectRoot: string, taskDir: string, agentType?: AgentType): string { const parts: string[] = []; + // prd.md and info.md — always included let prd = ""; try { prd = readFileSync(join(taskDir, "prd.md"), "utf-8"); } catch { } if (prd.trim()) parts.push(`## PRD\n\n${prd.trim()}`); @@ -90,7 +120,19 @@ function buildTaskContext(projectRoot: string, taskDir: string): string { try { info = readFileSync(join(taskDir, "info.md"), "utf-8"); } catch { } if (info.trim()) parts.push(`## Info\n\n${info.trim()}`); - for (const jsonlName of ["implement.jsonl", "check.jsonl"]) { + // Determine which jsonl files to read based on agent type + let jsonlNames: string[]; + if (agentType === "trellis-implement") { + jsonlNames = ["implement.jsonl"]; + } else if (agentType === "trellis-check") { + jsonlNames = ["check.jsonl"]; + } else if (agentType === "trellis-research") { + jsonlNames = []; // research agent gets only prd + info + } else { + jsonlNames = ["implement.jsonl", "check.jsonl"]; // main session: all + } + + for (const jsonlName of jsonlNames) { const jsonlPath = join(taskDir, jsonlName); if (!existsSync(jsonlPath)) continue; @@ -129,56 +171,30 @@ function buildTaskContext(projectRoot: string, taskDir: string): string { : ""; } -// --------------------------------------------------------------------------- -// Session overview — spawns get_context.py; non-fatal fallback on failure -// --------------------------------------------------------------------------- - -const SESSION_OVERVIEW_TIMEOUT_MS = 5000; -const SESSION_OVERVIEW_FALLBACK = - "Trellis workflow system active. Use skills and agents as directed by the workflow state."; - -function buildSessionOverview(projectRoot: string): string { - const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); - if (!existsSync(script)) return SESSION_OVERVIEW_FALLBACK; - - try { - const result = spawnSync("python3", [script, "--mode", "session-overview"], { - cwd: projectRoot, - encoding: "utf-8", - timeout: SESSION_OVERVIEW_TIMEOUT_MS, - windowsHide: true, - }); - if (result.status !== 0 || !result.stdout?.trim()) { - return SESSION_OVERVIEW_FALLBACK; - } - return result.stdout.trim(); - } catch { - return SESSION_OVERVIEW_FALLBACK; - } -} - // --------------------------------------------------------------------------- // Per-turn cache — prevents double-spawn when input + before_agent_start // fire in the same turn // --------------------------------------------------------------------------- +const SESSION_OVERVIEW_TEXT = + "Trellis workflow system active. Use skills and agents as directed by the workflow state."; + class TurnContextCache { private key: string | null = null; private timestamp = 0; private workflowMsg = ""; - private taskContext = ""; private static readonly TTL_MS = 1500; - get(projectRoot: string): { workflowMsg: string; taskContext: string } { + get(projectRoot: string): { workflowMsg: string } { const now = Date.now(); if ( this.key === projectRoot && now - this.timestamp < TurnContextCache.TTL_MS ) { - return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + return { workflowMsg: this.workflowMsg }; } - const { status, taskDir } = resolveActiveTaskStatus(projectRoot); + const { status } = resolveActiveTaskStatus(projectRoot); const workflowPath = join(projectRoot, ".trellis", "workflow.md"); let workflowMd = ""; @@ -196,13 +212,11 @@ class TurnContextCache { workflowBody = "Refer to workflow.md for current step."; } - const overview = buildSessionOverview(projectRoot); - this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${overview}\n</session-overview>`; - this.taskContext = taskDir ? buildTaskContext(projectRoot, taskDir) : ""; + this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`; this.key = projectRoot; this.timestamp = now; - return { workflowMsg: this.workflowMsg, taskContext: this.taskContext }; + return { workflowMsg: this.workflowMsg }; } } @@ -229,6 +243,20 @@ function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] { return blocks; } +// --------------------------------------------------------------------------- +// Sub-agent detection +// --------------------------------------------------------------------------- + +const TRELLIS_AGENTS = new Set(["trellis-implement", "trellis-check", "trellis-research"]); + +function detectAgentType(): AgentType { + const blocked = process.env.PI_BLOCKED_AGENT; + if (blocked && TRELLIS_AGENTS.has(blocked)) { + return blocked as AgentType; + } + return null; +} + // --------------------------------------------------------------------------- // Extension entry point // --------------------------------------------------------------------------- @@ -238,10 +266,49 @@ export default function(pi: ExtensionAPI): void { let projectRoot: string | null = null; const turnCache = new TurnContextCache(); + const agentType = detectAgentType(); + const isSubAgent = agentType !== null; pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); - if (projectRoot) { + if (!projectRoot) return; + + if (isSubAgent) { + // Sub-agent: inject precise task context once + const { taskDir } = resolveActiveTaskStatus(projectRoot); + if (taskDir) { + const taskContext = buildTaskContext(projectRoot, taskDir, agentType); + if (taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: taskContext, + display: false, + }); + } + } + } else { + // Main session: inject session context (global map) + task context + const sessionContext = buildSessionContext(projectRoot); + if (sessionContext) { + await pi.sendMessage({ + customType: "trellis-session-context", + content: sessionContext, + display: false, + }); + } + + const { taskDir } = resolveActiveTaskStatus(projectRoot); + if (taskDir) { + const taskContext = buildTaskContext(projectRoot, taskDir); + if (taskContext) { + await pi.sendMessage({ + customType: "trellis-task-context", + content: taskContext, + display: false, + }); + } + } + ctx.ui.notify("Trellis workflow system available", "info"); } }); @@ -252,16 +319,9 @@ export default function(pi: ExtensionAPI): void { } if (!projectRoot) return; + // Lightweight: workflow state only (no task context — already injected in session_start) const cached = turnCache.get(projectRoot); - if (cached.taskContext) { - await pi.sendMessage({ - customType: "trellis-task-context", - content: cached.taskContext, - display: false, - }); - } - return { message: { customType: "trellis-workflow-state", diff --git a/packages/cli/test/templates/omp.test.ts b/packages/cli/test/templates/omp.test.ts index 88ef587e7..fd9878b05 100644 --- a/packages/cli/test/templates/omp.test.ts +++ b/packages/cli/test/templates/omp.test.ts @@ -3,52 +3,74 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { - getAllAgents, - getExtensionTemplate, + getAllAgents, + getExtensionTemplate, } from "../../src/templates/omp/index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const templateDir = path.resolve(__dirname, "../../src/templates/omp"); describe("omp templates", () => { - it("provides the three Trellis sub-agent definitions", () => { - const agents = getAllAgents(); - expect(agents.map((agent) => agent.name).sort()).toEqual([ - "trellis-check", - "trellis-implement", - "trellis-research", - ]); - }); - - it("each agent has non-empty content and name", () => { - for (const agent of getAllAgents()) { - expect(agent.name.length).toBeGreaterThan(0); - expect(agent.content.length).toBeGreaterThan(0); - } - }); - - it("getExtensionTemplate returns a non-empty string", () => { - const extension = getExtensionTemplate(); - expect(extension.length).toBeGreaterThan(0); - }); - - it("extension template contains key markers for OMP integration", () => { - const extension = getExtensionTemplate(); - expect(extension).toContain("before_agent_start"); - expect(extension).toContain("input"); - expect(extension).toContain("session_start"); - expect(extension).toContain("ExtensionAPI"); - }); - - it("no settings.json or Python hooks exist in the template directory", () => { - // OMP is extension-backed: native provider auto-discovers .omp/ subdirs, - // so no settings.json is needed and no Python hooks should be present. - expect(fs.existsSync(path.join(templateDir, "settings.json"))).toBe(false); - expect(fs.existsSync(path.join(templateDir, "hooks"))).toBe(false); - - // Agents must not reference Python hook scripts - for (const agent of getAllAgents()) { - expect(agent.content).not.toContain("inject-subagent-context.py"); - } - }); + it("provides the three Trellis sub-agent definitions", () => { + const agents = getAllAgents(); + expect(agents.map((agent) => agent.name).sort()).toEqual([ + "trellis-check", + "trellis-implement", + "trellis-research", + ]); + }); + + it("each agent has non-empty content and name", () => { + for (const agent of getAllAgents()) { + expect(agent.name.length).toBeGreaterThan(0); + expect(agent.content.length).toBeGreaterThan(0); + } + }); + + it("getExtensionTemplate returns a non-empty string", () => { + const extension = getExtensionTemplate(); + expect(extension.length).toBeGreaterThan(0); + }); + + it("extension template contains key markers for OMP integration", () => { + const extension = getExtensionTemplate(); + expect(extension).toContain("before_agent_start"); + expect(extension).toContain("input"); + expect(extension).toContain("session_start"); + expect(extension).toContain("ExtensionAPI"); + }); + + it("extension template contains session context injection markers", () => { + const extension = getExtensionTemplate(); + // R1: Session start rich injection via get_context.py + expect(extension).toContain("buildSessionContext"); + expect(extension).toContain("trellis-session-context"); + expect(extension).toContain("get_context.py"); + expect(extension).toContain("session-context"); + }); + + it("extension template contains sub-agent precision injection markers", () => { + const extension = getExtensionTemplate(); + // R2: Sub-agent detection via PI_BLOCKED_AGENT + expect(extension).toContain("PI_BLOCKED_AGENT"); + expect(extension).toContain("detectAgentType"); + expect(extension).toContain("trellis-implement"); + expect(extension).toContain("trellis-check"); + expect(extension).toContain("trellis-research"); + // Agent-type-specific jsonl selection + expect(extension).toContain("implement.jsonl"); + expect(extension).toContain("check.jsonl"); + }); + + it("no settings.json or Python hooks exist in the template directory", () => { + // OMP is extension-backed: native provider auto-discovers .omp/ subdirs, + // so no settings.json is needed and no Python hooks should be present. + expect(fs.existsSync(path.join(templateDir, "settings.json"))).toBe(false); + expect(fs.existsSync(path.join(templateDir, "hooks"))).toBe(false); + + // Agents must not reference Python hook scripts + for (const agent of getAllAgents()) { + expect(agent.content).not.toContain("inject-subagent-context.py"); + } + }); }); From 4001a18575a08da0bea811582bb9d50f406e7b62 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Tue, 26 May 2026 20:01:54 +0800 Subject: [PATCH 07/19] chore(task): archive 05-26-omp-trellis-context-injection --- .../check.jsonl | 1 + .../implement.jsonl | 1 + .../prd.md | 138 ++++++++++++++++++ .../task.json | 26 ++++ 4 files changed, 166 insertions(+) create mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl create mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl create mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md create mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json diff --git a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl new file mode 100644 index 000000000..9dd3234a2 --- /dev/null +++ b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/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/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl new file mode 100644 index 000000000..9dd3234a2 --- /dev/null +++ b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/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/archive/2026-05/05-26-omp-trellis-context-injection/prd.md b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md new file mode 100644 index 000000000..595745a3f --- /dev/null +++ b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md @@ -0,0 +1,138 @@ +# OMP Trellis Extension: 补齐上下文注入和 SubAgent 精确注入 + +## Goal + +升级 `.omp/extensions/trellis/index.ts`,补齐两个核心能力: +1. Session Start 富注入 — 提供项目全局地图(git/spec/task/identity) +2. SubAgent 精确注入 — 根据 agent 类型选择性注入 task context,减少 token 浪费 + +保留 OMP 原生 Extension API 的架构优势(进程内执行、类型安全、`context` 事件)。 + +## Requirements + +### R1: Session Start 富注入 + +在 `session_start` 事件中通过 `pi.sendMessage()` 注入以下静态上下文(`display: false`): + +| 内容 | 来源 | 预估大小 | +|------|------|----------| +| Git branch + dirty count + 最近 3 commits | `spawnSync("git", ...)` | ~300B | +| Spec 索引 | `readdirSync(".trellis/spec/")` 递归一层 | ~500B | +| Active task 摘要 | 复用现有 `resolveActiveTaskStatus()` | ~100B | +| Developer identity | 读取 `.trellis/workspace/*/index.md` 匹配 git user | ~50B | + +总计 ~1-2KB,格式为 `<session-context>` XML tag。 + +**不注入**:workflow.md 内容、task context 详情、spec 文件正文(这些由 `before_agent_start` 每轮负责)。 + +**Compaction 后不重新注入** — `before_agent_start` 的 workflow state 已足够保底,session start 信息在 compaction 后可能已过时。 + +### R2: SubAgent 精确注入 + +Sub-agent session 会重新加载 extension。利用 `PI_BLOCKED_AGENT` env var 区分 agent 类型: + +| Agent 类型 | `PI_BLOCKED_AGENT` 值 | 注入内容 | +|-----------|----------------------|----------| +| `trellis-implement` | `"trellis-implement"` | `prd.md` + `info.md` + `implement.jsonl` 引用的文件 | +| `trellis-check` | `"trellis-check"` | `prd.md` + `info.md` + `check.jsonl` 引用的文件 | +| `trellis-research` | `"trellis-research"` | 仅 `prd.md` + `info.md` | +| 其他 / 主 session | 未设置或不匹配 | 全量注入(当前行为) | + +**注入时机分工(方案 C)**: +- `session_start` → 重量级 task context 文件注入(一次性) +- `before_agent_start` → 仅 workflow state 刷新(轻量,每轮) + +## Decision (ADR-lite) + +**Context**: 当前 extension 对主 session 和 sub-agent 使用完全相同的注入逻辑,导致 sub-agent 收到冗余信息(如 check agent 收到 implement.jsonl 的文件),且 session start 缺乏全局视图。 + +**Decision**: +- Session start 做"全局地图"(静态信息,一次性) +- before_agent_start 做"当前导航"(workflow state,轻量刷新) +- Sub-agent 中 session_start 做精确 task context 注入 +- 不引入配置层(OMP 已有 `disabledExtensions` 机制) +- Compaction 后不重新注入(before_agent_start 保底) + +**Consequences**: +- Sub-agent token 消耗降低(仅注入相关 jsonl) +- AI 首轮即有项目全局认知 +- 无新配置文件/新依赖 +- 若未来需要更细粒度控制,可通过 env var 扩展 + +## Acceptance Criteria + +- [ ] 主 session 首轮 AI 回复前,session context 已注入(含 git branch/dirty/commits + spec 索引 + task 摘要 + developer name) +- [ ] `trellis-implement` sub-agent 只收到 implement.jsonl 引用的文件,不含 check.jsonl +- [ ] `trellis-check` sub-agent 只收到 check.jsonl 引用的文件,不含 implement.jsonl +- [ ] `trellis-research` sub-agent 只收到 prd.md + info.md +- [ ] 主 session 的 before_agent_start 行为不变(backward compatible) +- [ ] 无 TypeScript 编译错误 +- [ ] session_start 中的 git/spec 操作耗时 <500ms +- [ ] `pnpm test` 通过(含更新后的 omp.test.ts marker 断言) + +## Definition of Done + +- TypeScript 编译通过 +- `pnpm test` 通过 +- 手动验证三类 sub-agent 的 context 差异 +- 代码风格与现有 extension 一致 +- 不引入外部依赖(仅 Node.js stdlib + OMP Extension API) + +## Out of Scope + +- 配置层 / settings.json(不需要,平台已有 disable 机制) +- Windows 路径规范化 +- 多平台检测 +- 新增 commands / skills +- 修改 agent `.md` 文件(增厚 prompt 为独立任务) +- 修改 `.trellis/scripts/` Python 脚本 +- `context` 事件裁剪优化(作为后续迭代) +- Compaction 后重新注入 + +## Technical Notes + +### 文件影响范围(3 个文件) + +| 文件 | 性质 | +|------|------| +| `.omp/extensions/trellis/index.ts` | 主改动 | +| `packages/cli/src/templates/omp/extensions/trellis/index.ts.txt` | 模板同步(内容 = 上面文件) | +| `packages/cli/test/templates/omp.test.ts` | 补充 marker 断言 | + +### 不需要改动的文件 + +- `configurators/omp.ts` — 通过 `getExtensionTemplate()` 自动获得新内容 +- `templates/omp/index.ts` — 导出函数签名不变 +- `types/ai-tools.ts` — 平台注册信息不变 +- `.trellis/scripts/common/cli_adapter.py` — 路径解析不变 +- `.trellis/scripts/common/task_store.py` — subagent config 不变 +- `regression.test.ts` — 现有断言测平台注册,不受内容变化影响 + +### 实现要点 + +1. **Session start 信息获取**: + - `spawnSync("git", ["branch", "--show-current"])` → branch name + - `spawnSync("git", ["status", "--porcelain"])` → dirty count + - `spawnSync("git", ["log", "--oneline", "-3"])` → recent commits + - `readdirSync(".trellis/spec/")` → spec package list, 每个 package 下一层列 layers + - 复用 `resolveActiveTaskStatus()` → task title + status + - 读取 `.trellis/workspace/` 下与 `git config user.name` 匹配的目录 + +2. **Sub-agent 检测**: + - `process.env.PI_BLOCKED_AGENT` → agent name(子 session 中自动设置) + - 存在此 env var → 当前是 sub-agent session + +3. **Task context 拆分**: + - 重构 `buildTaskContext()` 为 `buildTaskContext(projectRoot, taskDir, agentType?)` + - `agentType` 未传时保持全量行为(backward compatible) + - `agentType` 传入时只读取对应 jsonl + +4. **注入方式**: + - session_start: `pi.sendMessage({ customType: "trellis-session-context", content, display: false })` + - sub-agent task context: 同样在 session_start 中注入(一次性) + - before_agent_start: 保持仅 workflow state 注入(sub-agent 中也生效) + +5. **模板同步规则**: + - `.omp/extensions/trellis/index.ts` 与 `packages/cli/src/templates/omp/extensions/trellis/index.ts.txt` 内容完全相同 + - `replacePythonCommandLiterals()` 由 configurator 在 `trellis init --omp` 时自动应用 + - 模板源文件中直接写 `python3` diff --git a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json new file mode 100644 index 000000000..555fb080c --- /dev/null +++ b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json @@ -0,0 +1,26 @@ +{ + "id": "omp-trellis-context-injection", + "name": "omp-trellis-context-injection", + "title": "OMP Trellis Extension: 补齐上下文注入、配置层和 SubAgent 精确注入", + "description": "", + "status": "completed", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "VinciWu557", + "assignee": "VinciWu557", + "createdAt": "2026-05-26", + "completedAt": "2026-05-26", + "branch": null, + "base_branch": "feat/support-oh-my-pi", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file From 8a15839b34c57f09dee217be32f10d07b0884b7b Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Tue, 26 May 2026 20:02:04 +0800 Subject: [PATCH 08/19] chore: record journal --- .trellis/workspace/VinciWu557/index.md | 4 +-- .trellis/workspace/VinciWu557/journal-1.md | 33 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.trellis/workspace/VinciWu557/index.md b/.trellis/workspace/VinciWu557/index.md index 399f9178b..700598082 100644 --- a/.trellis/workspace/VinciWu557/index.md +++ b/.trellis/workspace/VinciWu557/index.md @@ -2,5 +2,5 @@ <!-- @@@auto:current-status --> - **Active File**: `journal-1.md` -- **Total Sessions**: 2 -- **Last Active**: 2026-05-25 \ No newline at end of file +- **Total Sessions**: 3 +- **Last Active**: 2026-05-26 \ No newline at end of file diff --git a/.trellis/workspace/VinciWu557/journal-1.md b/.trellis/workspace/VinciWu557/journal-1.md index 6343c998e..865db156d 100644 --- a/.trellis/workspace/VinciWu557/journal-1.md +++ b/.trellis/workspace/VinciWu557/journal-1.md @@ -71,3 +71,36 @@ Added Oh My Pi (omp) platform support to Trellis, implementing Phase 1 + Phase 2 ### Next Steps - None - task complete + + +## Session 3: OMP extension: session-start rich injection + sub-agent precision + +**Date**: 2026-05-26 +**Task**: OMP extension: session-start rich injection + sub-agent precision +**Branch**: `feat/support-oh-my-pi` + +### Summary + +Upgraded .omp/extensions/trellis/index.ts: session_start now invokes get_context.py for full project map; sub-agents receive only their relevant jsonl files via PI_BLOCKED_AGENT detection; before_agent_start simplified to workflow-state only. Fixed dead buildSessionOverview calling non-existent script mode. All 1042 tests pass. + +### Main Changes + +(Add details) + +### Git Commits + +| Hash | Message | +|------|---------| +| `307234f` | (see git log) | + +### Testing + +- [OK] (Add test results) + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete From 49dca22e971056aa82ec2c04a186b9db38e382d1 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Wed, 27 May 2026 11:35:56 +0800 Subject: [PATCH 09/19] feat(omp): optimize extension with context handler and compaction-aware injection - Add context event handler as safety net for post-compaction continuations - Add session_before_compact listener to track compaction boundaries - Use reverse-scan with fast-path skip when no compaction occurred - Simplify input handler to only pre-warm cache (no message injection) - before_agent_start remains the persistent injection point --- .omp/extensions/trellis/index.ts | 65 +++++++++++++++---- .../omp/extensions/trellis/index.ts.txt | 65 +++++++++++++++---- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts index 4bf60e733..e68db0806 100644 --- a/.omp/extensions/trellis/index.ts +++ b/.omp/extensions/trellis/index.ts @@ -172,8 +172,8 @@ function buildTaskContext(projectRoot: string, taskDir: string, agentType?: Agen } // --------------------------------------------------------------------------- -// Per-turn cache — prevents double-spawn when input + before_agent_start -// fire in the same turn +// Per-turn cache — prevents redundant workflow-state resolution within a +// single event cascade (input, before_agent_start, and context fire closely) // --------------------------------------------------------------------------- const SESSION_OVERVIEW_TEXT = @@ -269,6 +269,11 @@ export default function(pi: ExtensionAPI): void { const agentType = detectAgentType(); const isSubAgent = agentType !== null; + // Tracks compaction boundaries — context handler skips scanning when no + // compaction has occurred since last injection. + let lastCompactionTs = 0; + let lastInjectionTs = 0; + pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); if (!projectRoot) return; @@ -313,14 +318,19 @@ export default function(pi: ExtensionAPI): void { } }); + pi.on("session_before_compact", async () => { + lastCompactionTs = Date.now(); + }); + pi.on("before_agent_start", async (_event, ctx) => { if (!projectRoot) { projectRoot = findProjectRoot(ctx.cwd); } if (!projectRoot) return; - // Lightweight: workflow state only (no task context — already injected in session_start) + // Persistent injection: workflow state for this turn const cached = turnCache.get(projectRoot); + lastInjectionTs = Date.now(); return { message: { @@ -331,22 +341,49 @@ export default function(pi: ExtensionAPI): void { }; }); - pi.on("input", async (_event, ctx) => { - if (!projectRoot) { - projectRoot = findProjectRoot(ctx.cwd); - } - if (!projectRoot) return { action: "continue" }; + // context fires before EVERY LLM API call (including tool-use continuations + // and post-compaction agent.continue() paths). Acts as a safety net when + // before_agent_start's persisted message was removed by compaction. + pi.on("context", async (event) => { + if (!projectRoot) return; + + // Fast path: no compaction since last injection — message is still present + if (lastInjectionTs > lastCompactionTs) return; + const cached = turnCache.get(projectRoot); - if (cached.workflowMsg) { - return { - action: "continue", - message: { + if (!cached.workflowMsg) return; + + // Post-compaction: reverse-scan to confirm absence before injecting + const messages = event.messages as { role?: string; customType?: string }[]; + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === "custom" && messages[i].customType === "trellis-workflow-state") { + lastInjectionTs = Date.now(); + return; + } + } + + lastInjectionTs = Date.now(); + return { + messages: [ + ...event.messages, + { + role: "custom", customType: "trellis-workflow-state", content: cached.workflowMsg, - display: false, + timestamp: Date.now(), }, - }; + ], + }; + }); + + pi.on("input", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); } + // Resolve projectRoot on first input if session_start missed it + if (!projectRoot) return { action: "continue" }; + // Pre-warm the cache so before_agent_start and context can use it + turnCache.get(projectRoot); return { action: "continue" }; }); } diff --git a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt index 4bf60e733..e68db0806 100644 --- a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -172,8 +172,8 @@ function buildTaskContext(projectRoot: string, taskDir: string, agentType?: Agen } // --------------------------------------------------------------------------- -// Per-turn cache — prevents double-spawn when input + before_agent_start -// fire in the same turn +// Per-turn cache — prevents redundant workflow-state resolution within a +// single event cascade (input, before_agent_start, and context fire closely) // --------------------------------------------------------------------------- const SESSION_OVERVIEW_TEXT = @@ -269,6 +269,11 @@ export default function(pi: ExtensionAPI): void { const agentType = detectAgentType(); const isSubAgent = agentType !== null; + // Tracks compaction boundaries — context handler skips scanning when no + // compaction has occurred since last injection. + let lastCompactionTs = 0; + let lastInjectionTs = 0; + pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); if (!projectRoot) return; @@ -313,14 +318,19 @@ export default function(pi: ExtensionAPI): void { } }); + pi.on("session_before_compact", async () => { + lastCompactionTs = Date.now(); + }); + pi.on("before_agent_start", async (_event, ctx) => { if (!projectRoot) { projectRoot = findProjectRoot(ctx.cwd); } if (!projectRoot) return; - // Lightweight: workflow state only (no task context — already injected in session_start) + // Persistent injection: workflow state for this turn const cached = turnCache.get(projectRoot); + lastInjectionTs = Date.now(); return { message: { @@ -331,22 +341,49 @@ export default function(pi: ExtensionAPI): void { }; }); - pi.on("input", async (_event, ctx) => { - if (!projectRoot) { - projectRoot = findProjectRoot(ctx.cwd); - } - if (!projectRoot) return { action: "continue" }; + // context fires before EVERY LLM API call (including tool-use continuations + // and post-compaction agent.continue() paths). Acts as a safety net when + // before_agent_start's persisted message was removed by compaction. + pi.on("context", async (event) => { + if (!projectRoot) return; + + // Fast path: no compaction since last injection — message is still present + if (lastInjectionTs > lastCompactionTs) return; + const cached = turnCache.get(projectRoot); - if (cached.workflowMsg) { - return { - action: "continue", - message: { + if (!cached.workflowMsg) return; + + // Post-compaction: reverse-scan to confirm absence before injecting + const messages = event.messages as { role?: string; customType?: string }[]; + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === "custom" && messages[i].customType === "trellis-workflow-state") { + lastInjectionTs = Date.now(); + return; + } + } + + lastInjectionTs = Date.now(); + return { + messages: [ + ...event.messages, + { + role: "custom", customType: "trellis-workflow-state", content: cached.workflowMsg, - display: false, + timestamp: Date.now(), }, - }; + ], + }; + }); + + pi.on("input", async (_event, ctx) => { + if (!projectRoot) { + projectRoot = findProjectRoot(ctx.cwd); } + // Resolve projectRoot on first input if session_start missed it + if (!projectRoot) return { action: "continue" }; + // Pre-warm the cache so before_agent_start and context can use it + turnCache.get(projectRoot); return { action: "continue" }; }); } From 5b370321a831504f137ed44b5d923503907a4ef1 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Thu, 28 May 2026 15:10:42 +0800 Subject: [PATCH 10/19] chore(omp): add model: pi/task to implement and research agent templates --- packages/cli/src/templates/omp/agents/trellis-implement.md | 1 + packages/cli/src/templates/omp/agents/trellis-research.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/cli/src/templates/omp/agents/trellis-implement.md b/packages/cli/src/templates/omp/agents/trellis-implement.md index a022524c2..badee9a56 100644 --- a/packages/cli/src/templates/omp/agents/trellis-implement.md +++ b/packages/cli/src/templates/omp/agents/trellis-implement.md @@ -3,6 +3,7 @@ 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, search, ast_grep, lsp +model: pi/task --- # Implement Agent diff --git a/packages/cli/src/templates/omp/agents/trellis-research.md b/packages/cli/src/templates/omp/agents/trellis-research.md index 372167bd2..d7ec4f263 100644 --- a/packages/cli/src/templates/omp/agents/trellis-research.md +++ b/packages/cli/src/templates/omp/agents/trellis-research.md @@ -4,6 +4,7 @@ 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, search, web_search +model: pi/task --- # Research Agent From ba2bad27c5ea8a2ec2cc1847ce674c7a3989dcb6 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Thu, 28 May 2026 15:11:13 +0800 Subject: [PATCH 11/19] chore(omp): add model: pi/task to project-level implement and research agents --- .omp/agents/trellis-implement.md | 1 + .omp/agents/trellis-research.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.omp/agents/trellis-implement.md b/.omp/agents/trellis-implement.md index a022524c2..badee9a56 100644 --- a/.omp/agents/trellis-implement.md +++ b/.omp/agents/trellis-implement.md @@ -3,6 +3,7 @@ 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, search, ast_grep, lsp +model: pi/task --- # Implement Agent diff --git a/.omp/agents/trellis-research.md b/.omp/agents/trellis-research.md index 372167bd2..d7ec4f263 100644 --- a/.omp/agents/trellis-research.md +++ b/.omp/agents/trellis-research.md @@ -4,6 +4,7 @@ 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, search, web_search +model: pi/task --- # Research Agent From dcdda3f4fa0911164b142901a582c3237a5d7f1b Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Wed, 3 Jun 2026 15:13:53 +0800 Subject: [PATCH 12/19] fix(omp): add YAML frontmatter to generated command files OMP commands require `description` (and optional `argument-hint`) in YAML frontmatter for the `/` command picker. The configurator was writing raw template body without wrapping. Add `wrapWithOmpFrontmatter()` to shared.ts, wire it into both `collectOmpTemplates()` and `configureOmp()`, and add unit + integration tests. --- .omp/commands/trellis-continue.md | 6 +- .omp/commands/trellis-finish-work.md | 5 +- .../spec/cli/backend/platform-integration.md | 6 +- packages/cli/src/configurators/omp.ts | 8 +- packages/cli/src/configurators/shared.ts | 31 ++++ .../cli/test/configurators/shared.test.ts | 82 ++++++++-- packages/cli/test/templates/omp.test.ts | 141 +++++++++++------- 7 files changed, 198 insertions(+), 81 deletions(-) diff --git a/.omp/commands/trellis-continue.md b/.omp/commands/trellis-continue.md index 3a60538f0..666ad194b 100644 --- a/.omp/commands/trellis-continue.md +++ b/.omp/commands/trellis-continue.md @@ -1,9 +1,9 @@ -# Continue Current Task +--- +description: Resume work on the current task at the right workflow phase +--- Resume work on the current task — pick up at the right phase/step in `.trellis/workflow.md`. ---- - ## Step 1: Load Current Context ```bash diff --git a/.omp/commands/trellis-finish-work.md b/.omp/commands/trellis-finish-work.md index ab751c687..d09b85ba3 100644 --- a/.omp/commands/trellis-finish-work.md +++ b/.omp/commands/trellis-finish-work.md @@ -1,4 +1,7 @@ -# Finish Work +--- +description: Archive the active task and record the session journal +argument-hint: [task-name] +--- 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. diff --git a/.trellis/spec/cli/backend/platform-integration.md b/.trellis/spec/cli/backend/platform-integration.md index 004deb1fd..591b37a18 100644 --- a/.trellis/spec/cli/backend/platform-integration.md +++ b/.trellis/spec/cli/backend/platform-integration.md @@ -802,6 +802,7 @@ These are now **automatically derived** from the registry: | Devin | `/trellis-xxx` | Markdown (`.md`) + `SKILL.md` | `/trellis-finish-work` | | Pi Agent | `/trellis-xxx` prompt templates + `/skill:<name>` skills | Markdown (`.md`) + `SKILL.md` + TypeScript extension | `/trellis-finish-work` | | Trae IDE | `/trellis-xxx` commands + skills | Markdown (`.md` with frontmatter) + `SKILL.md` + `hooks.json` | `/trellis-finish-work` | +| OMP | `/trellis-xxx` | Markdown (`.md` with YAML frontmatter: `description` + optional `argument-hint`) | `/trellis-finish-work` | When creating platform templates, ensure references match the platform's interaction format and file format. @@ -817,7 +818,7 @@ Commands emitted by `resolveCommands(ctx)` / `resolveAllAsSkills(ctx)` / `resolv **Rule**: filter is by `ctx.agentCapable && ctx.hasHooks` — **both flags required** (changed in 0.6.4; the prior single-flag rule silently dropped `start` from Codex / ZCode / OpenCode / Reasonix). `agentCapable` alone is not a proxy for "has a session-start mechanism" because four agent-capable platforms ship without a SessionStart-equivalent hook and rely on user-invocable `start` instead. -- `agentCapable && hasHooks`: `claude-code, cursor, kiro, gemini, qoder, codebuddy, copilot, droid, pi, trae` +- `agentCapable && hasHooks`: `claude-code, cursor, kiro, gemini, qoder, codebuddy, copilot, droid, pi, trae, omp` - `agentCapable && !hasHooks`: `codex, opencode, reasonix, zcode` — Codex has a UserPromptSubmit hook but no SessionStart; OpenCode has a `plugins/session-start.js` plugin but registry-`hasHooks` is reserved for the SessionStart-style hook protocol; ZCode and Reasonix have neither. - `!agentCapable`: `kilo, antigravity, devin` @@ -902,13 +903,14 @@ Sub-agents on class-2 platforms run as **separate sessions** with their own sess When changing the prelude, the dispatch protocol, or the `session-fallback` semantics, all three layers must stay aligned. `regression.test.ts > [issue-225]` and `regression.test.ts > [session-fallback]` are the contract tests; `templates/trellis.test.ts > [issue-225]` asserts the workflow.md breadcrumb still carries the protocol. Manual e2e runbook lives in the historical task `.trellis/tasks/<archive>/05-04-fix-codex-subagent-missing-active-task/manual-verify.md`. -### Class-3 — Extension-backed (1 platform) +### Class-3 — Extension-backed (2 platforms) Platform can expose hook-equivalent events and custom tools through a project-local extension. Trellis owns the sub-agent tool and the context injection path. Unlike class-1 (which only handles sub-agent context) and class-2 (which only handles sub-agent prelude), class-3 owns **three** injection points: per-user-turn context, per-agent-invocation system prompt augmentation, and per-Bash-tool-call session-identity prefixing. | Platform | Extension surface | Context delivery | |---|---|---| | Pi Agent | `.pi/extensions/trellis/index.ts` events + `trellis_subagent` tool | extension builds prompt from `.pi/agents/*.md`, `prd.md`, `design.md` if present, `implement.md` if present, and JSONL-referenced files via `buildContext()`; injects per-turn `<workflow-state>` + `<session-overview>` via `getTurnCtx()` into user input and agent startup context; agent definitions also receive the pull-based prelude as a fallback | +| Oh My Pi | `.omp/extensions/trellis/index.ts` events | extension injects session-aware task context through `before_agent_start`, keeps per-turn state warm through `input` / `context`, and does not install Trellis Python hooks | See **"Class-3 injection points (Pi extension)"** and **"Cross-platform consistency invariant"** below for the runtime contract details. diff --git a/packages/cli/src/configurators/omp.ts b/packages/cli/src/configurators/omp.ts index ca29bd474..ea31a2596 100644 --- a/packages/cli/src/configurators/omp.ts +++ b/packages/cli/src/configurators/omp.ts @@ -7,6 +7,7 @@ import { resolveCommands, resolveBundledSkills, resolveSkills, + wrapWithOmpFrontmatter, writeAgents, writeSkills, } from "./shared.js"; @@ -23,7 +24,10 @@ export function collectOmpTemplates(): Map<string, string> { // Commands → .omp/commands/ for (const command of resolveCommands(ctx)) { - files.set(`.omp/commands/trellis-${command.name}.md`, command.content); + files.set( + `.omp/commands/trellis-${command.name}.md`, + wrapWithOmpFrontmatter(command.name, command.content), + ); } // Skills → .omp/skills/ @@ -63,7 +67,7 @@ export async function configureOmp(cwd: string): Promise<void> { for (const command of resolveCommands(ctx)) { await writeFile( path.join(configRoot, "commands", `trellis-${command.name}.md`), - command.content, + wrapWithOmpFrontmatter(command.name, command.content), ); } diff --git a/packages/cli/src/configurators/shared.ts b/packages/cli/src/configurators/shared.ts index f585f7b1b..0a3af9685 100644 --- a/packages/cli/src/configurators/shared.ts +++ b/packages/cli/src/configurators/shared.ts @@ -300,6 +300,37 @@ export function wrapWithCommandFrontmatter( return `---\nname: ${name}\ndescription: ${description}\n---\n\n${content}`; } +/** + * Argument-hint values for commands that accept positional args. + * Used by OMP platform's YAML frontmatter. + */ +const COMMAND_ARGUMENT_HINTS: Record<string, string> = { + "finish-work": "[task-name]", +}; + +/** + * Wrap resolved command content with OMP-style YAML frontmatter. + * OMP uses `description` (required) + optional `argument-hint`. + * The leading `# Title` heading from the source template is stripped + * because OMP's frontmatter replaces its role. + */ +export function wrapWithOmpFrontmatter(name: string, content: string): string { + const baseName = name.replace(/^trellis-/, ""); + const description = COMMAND_DESCRIPTIONS[baseName]; + if (!description) { + throw new Error( + `Missing command description for "${baseName}". Add it to COMMAND_DESCRIPTIONS in shared.ts.`, + ); + } + // Strip leading H1 + blank line from template body + const body = content.replace(/^# [^\n]+\n\n/, ""); + const hint = COMMAND_ARGUMENT_HINTS[baseName]; + const frontmatter = hint + ? `---\ndescription: ${description}\nargument-hint: ${hint}\n---` + : `---\ndescription: ${description}\n---`; + return `${frontmatter}\n\n${body}`; +} + // --------------------------------------------------------------------------- // Shared configurator helpers // --------------------------------------------------------------------------- diff --git a/packages/cli/test/configurators/shared.test.ts b/packages/cli/test/configurators/shared.test.ts index 7fca1a3ec..2aa30f01f 100644 --- a/packages/cli/test/configurators/shared.test.ts +++ b/packages/cli/test/configurators/shared.test.ts @@ -3,9 +3,11 @@ import { getPythonCommandForPlatform, replacePythonCommandLiterals, resolveAllAsSkillsNeutral, + resolveCommands, resolvePlaceholders, resolvePlaceholdersNeutral, resolveSkillsNeutral, + wrapWithOmpFrontmatter, } from "../../src/configurators/shared.js"; import { AI_TOOLS } from "../../src/types/ai-tools.js"; import type { TemplateContext } from "../../src/types/ai-tools.js"; @@ -70,9 +72,9 @@ describe("replacePythonCommandLiterals", () => { it("replaces multiple occurrences on win32", () => { mockPlatform("win32"); - expect( - replacePythonCommandLiterals("python3 a.py && python3 b.py"), - ).toBe("python a.py && python b.py"); + expect(replacePythonCommandLiterals("python3 a.py && python3 b.py")).toBe( + "python a.py && python b.py", + ); }); it("preserves shebang lines on win32", () => { @@ -120,13 +122,13 @@ describe("replacePythonCommandLiterals", () => { const input = [ "#!/usr/bin/env python3", "# comment about python3", - "exec python3 \"$0\" \"$@\"", + 'exec python3 "$0" "$@"', "python3 ./.trellis/scripts/task.py", ].join("\n"); const expected = [ "#!/usr/bin/env python3", "# comment about python", - "exec python \"$0\" \"$@\"", + 'exec python "$0" "$@"', "python ./.trellis/scripts/task.py", ].join("\n"); expect(replacePythonCommandLiterals(input)).toBe(expected); @@ -157,7 +159,9 @@ describe("resolvePlaceholders", () => { it("resolves {{PYTHON_CMD}}", () => { const result = resolvePlaceholders("run {{PYTHON_CMD}} script.py"); const expected = - process.platform === "win32" ? "run python script.py" : "run python3 script.py"; + process.platform === "win32" + ? "run python script.py" + : "run python3 script.py"; expect(result).toBe(expected); }); @@ -205,9 +209,9 @@ describe("resolvePlaceholders", () => { }); it("handles hyphenated command names", () => { - expect( - resolvePlaceholders("{{CMD_REF:finish-work}}", claudeCtx), - ).toBe("/trellis:finish-work"); + expect(resolvePlaceholders("{{CMD_REF:finish-work}}", claudeCtx)).toBe( + "/trellis:finish-work", + ); expect( resolvePlaceholders("{{CMD_REF:check-cross-layer}}", codexCtx), ).toBe("$check-cross-layer"); @@ -366,7 +370,8 @@ describe("resolvePlaceholders", () => { describe("blank line cleanup", () => { it("collapses 3+ consecutive blank lines to 2", () => { - const template = "A\n\n{{#AGENT_CAPABLE}}\nRemoved\n{{/AGENT_CAPABLE}}\n\nB"; + const template = + "A\n\n{{#AGENT_CAPABLE}}\nRemoved\n{{/AGENT_CAPABLE}}\n\nB"; const result = resolvePlaceholders(template, cursorCtx); expect(result).not.toMatch(/\n{3,}/); expect(result).toContain("A"); @@ -473,12 +478,12 @@ describe("resolvePlaceholdersNeutral", () => { it("still resolves {{EXECUTOR_AI}} and {{USER_ACTION_LABEL}} per platform", () => { // Defensive: not used in current shared skills, but kept functional for // future templates. - expect( - resolvePlaceholdersNeutral("{{EXECUTOR_AI}}", claudeCtx), - ).toBe("Bash scripts or Task calls"); - expect( - resolvePlaceholdersNeutral("{{USER_ACTION_LABEL}}", codexCtx), - ).toBe("Skills"); + expect(resolvePlaceholdersNeutral("{{EXECUTOR_AI}}", claudeCtx)).toBe( + "Bash scripts or Task calls", + ); + expect(resolvePlaceholdersNeutral("{{USER_ACTION_LABEL}}", codexCtx)).toBe( + "Skills", + ); }); it("still applies conditional blocks per context", () => { @@ -567,3 +572,48 @@ describe("resolveSkillsNeutral / resolveAllAsSkillsNeutral", () => { } }); }); + +// --------------------------------------------------------------------------- +// wrapWithOmpFrontmatter — OMP command YAML frontmatter +// --------------------------------------------------------------------------- + +describe("wrapWithOmpFrontmatter", () => { + it("wraps continue command with description-only frontmatter", () => { + const content = + "# Continue Current Task\n\nResume work on the current task."; + const result = wrapWithOmpFrontmatter("continue", content); + expect(result).toMatch(/^---\ndescription: .+\n---\n\n/); + expect(result).not.toContain("# Continue Current Task"); + expect(result).toContain("Resume work on the current task."); + expect(result).not.toContain("argument-hint"); + }); + + it("wraps finish-work command with description + argument-hint", () => { + const content = "# Finish Work\n\nWrap up the current session."; + const result = wrapWithOmpFrontmatter("finish-work", content); + expect(result).toMatch( + /^---\ndescription: .+\nargument-hint: \[task-name\]\n---\n\n/, + ); + expect(result).not.toContain("# Finish Work"); + expect(result).toContain("Wrap up the current session."); + }); + + it("strips trellis- prefix before looking up description", () => { + const content = "# Continue Current Task\n\nBody text."; + const result = wrapWithOmpFrontmatter("trellis-continue", content); + expect(result).toMatch(/^---\ndescription: /); + expect(result).toContain("Body text."); + }); + + it("throws on unknown command name", () => { + expect(() => wrapWithOmpFrontmatter("nonexistent", "body")).toThrow( + /Missing command description/, + ); + }); + + it("preserves body content after H1 removal", () => { + const content = "# Title\n\nLine 1\n\nLine 2\n\n## Section\n\nMore text."; + const result = wrapWithOmpFrontmatter("continue", content); + expect(result).toContain("Line 1\n\nLine 2\n\n## Section\n\nMore text."); + }); +}); diff --git a/packages/cli/test/templates/omp.test.ts b/packages/cli/test/templates/omp.test.ts index fd9878b05..659ed656e 100644 --- a/packages/cli/test/templates/omp.test.ts +++ b/packages/cli/test/templates/omp.test.ts @@ -3,74 +3,101 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { - getAllAgents, - getExtensionTemplate, + getAllAgents, + getExtensionTemplate, } from "../../src/templates/omp/index.js"; +import { collectOmpTemplates } from "../../src/configurators/omp.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const templateDir = path.resolve(__dirname, "../../src/templates/omp"); describe("omp templates", () => { - it("provides the three Trellis sub-agent definitions", () => { - const agents = getAllAgents(); - expect(agents.map((agent) => agent.name).sort()).toEqual([ - "trellis-check", - "trellis-implement", - "trellis-research", - ]); - }); + it("provides the three Trellis sub-agent definitions", () => { + const agents = getAllAgents(); + expect(agents.map((agent) => agent.name).sort()).toEqual([ + "trellis-check", + "trellis-implement", + "trellis-research", + ]); + }); - it("each agent has non-empty content and name", () => { - for (const agent of getAllAgents()) { - expect(agent.name.length).toBeGreaterThan(0); - expect(agent.content.length).toBeGreaterThan(0); - } - }); + it("each agent has non-empty content and name", () => { + for (const agent of getAllAgents()) { + expect(agent.name.length).toBeGreaterThan(0); + expect(agent.content.length).toBeGreaterThan(0); + } + }); - it("getExtensionTemplate returns a non-empty string", () => { - const extension = getExtensionTemplate(); - expect(extension.length).toBeGreaterThan(0); - }); + it("getExtensionTemplate returns a non-empty string", () => { + const extension = getExtensionTemplate(); + expect(extension.length).toBeGreaterThan(0); + }); - it("extension template contains key markers for OMP integration", () => { - const extension = getExtensionTemplate(); - expect(extension).toContain("before_agent_start"); - expect(extension).toContain("input"); - expect(extension).toContain("session_start"); - expect(extension).toContain("ExtensionAPI"); - }); + it("extension template contains key markers for OMP integration", () => { + const extension = getExtensionTemplate(); + expect(extension).toContain("before_agent_start"); + expect(extension).toContain("input"); + expect(extension).toContain("session_start"); + expect(extension).toContain("ExtensionAPI"); + }); - it("extension template contains session context injection markers", () => { - const extension = getExtensionTemplate(); - // R1: Session start rich injection via get_context.py - expect(extension).toContain("buildSessionContext"); - expect(extension).toContain("trellis-session-context"); - expect(extension).toContain("get_context.py"); - expect(extension).toContain("session-context"); - }); + it("extension template contains session context injection markers", () => { + const extension = getExtensionTemplate(); + // R1: Session start rich injection via get_context.py + expect(extension).toContain("buildSessionContext"); + expect(extension).toContain("trellis-session-context"); + expect(extension).toContain("get_context.py"); + expect(extension).toContain("session-context"); + }); - it("extension template contains sub-agent precision injection markers", () => { - const extension = getExtensionTemplate(); - // R2: Sub-agent detection via PI_BLOCKED_AGENT - expect(extension).toContain("PI_BLOCKED_AGENT"); - expect(extension).toContain("detectAgentType"); - expect(extension).toContain("trellis-implement"); - expect(extension).toContain("trellis-check"); - expect(extension).toContain("trellis-research"); - // Agent-type-specific jsonl selection - expect(extension).toContain("implement.jsonl"); - expect(extension).toContain("check.jsonl"); - }); + it("extension template contains sub-agent precision injection markers", () => { + const extension = getExtensionTemplate(); + // R2: Sub-agent detection via PI_BLOCKED_AGENT + expect(extension).toContain("PI_BLOCKED_AGENT"); + expect(extension).toContain("detectAgentType"); + expect(extension).toContain("trellis-implement"); + expect(extension).toContain("trellis-check"); + expect(extension).toContain("trellis-research"); + // Agent-type-specific jsonl selection + expect(extension).toContain("implement.jsonl"); + expect(extension).toContain("check.jsonl"); + }); - it("no settings.json or Python hooks exist in the template directory", () => { - // OMP is extension-backed: native provider auto-discovers .omp/ subdirs, - // so no settings.json is needed and no Python hooks should be present. - expect(fs.existsSync(path.join(templateDir, "settings.json"))).toBe(false); - expect(fs.existsSync(path.join(templateDir, "hooks"))).toBe(false); + it("no settings.json or Python hooks exist in the template directory", () => { + // OMP is extension-backed: native provider auto-discovers .omp/ subdirs, + // so no settings.json is needed and no Python hooks should be present. + expect(fs.existsSync(path.join(templateDir, "settings.json"))).toBe(false); + expect(fs.existsSync(path.join(templateDir, "hooks"))).toBe(false); - // Agents must not reference Python hook scripts - for (const agent of getAllAgents()) { - expect(agent.content).not.toContain("inject-subagent-context.py"); - } - }); + // Agents must not reference Python hook scripts + for (const agent of getAllAgents()) { + expect(agent.content).not.toContain("inject-subagent-context.py"); + } + }); +}); + +describe("omp command frontmatter", () => { + it("collectOmpTemplates produces commands with YAML frontmatter", () => { + const templates = collectOmpTemplates(); + const continueCmd = templates.get(".omp/commands/trellis-continue.md"); + const finishCmd = templates.get(".omp/commands/trellis-finish-work.md"); + + expect(continueCmd).toBeDefined(); + expect(finishCmd).toBeDefined(); + + // Both must start with YAML frontmatter + expect(continueCmd).toMatch(/^---\ndescription: .+\n---\n\n/); + expect(finishCmd).toMatch( + /^---\ndescription: .+\nargument-hint: .+\n---\n\n/, + ); + + // Neither should retain the H1 heading from the source template + expect(continueCmd).not.toMatch(/^---[\s\S]*?---\n\n# /); + expect(finishCmd).not.toMatch(/^---[\s\S]*?---\n\n# /); + }); + + it("collectOmpTemplates does not emit a start command", () => { + const templates = collectOmpTemplates(); + expect(templates.has(".omp/commands/trellis-start.md")).toBe(false); + }); }); From e32cb7dbe44c07660262b4a022a7f0d1cf27a228 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Wed, 3 Jun 2026 15:15:10 +0800 Subject: [PATCH 13/19] chore(task): archive 06-03-omp-configurator-missing-command-frontmatter --- .../check.jsonl | 2 + .../implement.jsonl | 2 + .../prd.md | 42 +++++++++++++++++++ .../task.json | 26 ++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl create mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl create mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md create mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl new file mode 100644 index 000000000..154b5eefb --- /dev/null +++ b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl @@ -0,0 +1,2 @@ +{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Verify command format matches OMP spec and doesn't break collectTemplates hash comparison"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Verify test follows project conventions"} diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl new file mode 100644 index 000000000..1a570192a --- /dev/null +++ b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl @@ -0,0 +1,2 @@ +{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform configurator architecture, command format table, wrapWithCommandFrontmatter pattern"} +{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Unit test conventions for writing configurator tests"} diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md new file mode 100644 index 000000000..66b62e0fa --- /dev/null +++ b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md @@ -0,0 +1,42 @@ +# OMP Configurator Missing Command Frontmatter + +## Goal + +`configureOmp()` and `collectOmpTemplates()` write command files without YAML frontmatter. OMP platform requires `---\ndescription: ...\n---` (and optionally `argument-hint:`) at the top for the `/` command picker to work. Fix the configurator so `trellis init`/`trellis update` produces correctly formatted OMP commands. + +## Requirements + +- Commands written to `.omp/commands/` must include YAML frontmatter with `description` field +- Commands that accept positional args should include `argument-hint` field (e.g. `finish-work` has `[task-name]`) +- The `# Title` H1 heading in source templates must be stripped (frontmatter replaces its role) +- The existing `COMMAND_DESCRIPTIONS` map in `shared.ts` should be the source of truth for `description` values +- Argument hints need a new data source (small map or extension of existing structure) + +## Acceptance Criteria + +- [ ] `collectOmpTemplates()` output for each command starts with valid YAML frontmatter +- [ ] `configureOmp()` writes commands with matching frontmatter +- [ ] Generated output matches the format user hand-fixed: `---\ndescription: ...\n[argument-hint: ...]\n---\n\n<body without H1>` +- [ ] Existing tests pass; new unit test covers OMP command frontmatter generation + +## Technical Approach + +Add an OMP-specific command wrapper (analogous to `wrapWithCommandFrontmatter` used by Qoder). Two sub-decisions: + +1. **Strip H1**: Source templates start with `# Title\n\n<body>`. OMP format uses frontmatter instead of H1. Strip the first `# ...` line + trailing blank when wrapping. +2. **Argument hints**: Add an `ARGUMENT_HINTS` map (or extend `COMMAND_DESCRIPTIONS` to a richer object). Only `finish-work` currently needs one (`[task-name]`). + +Implementation site: `packages/cli/src/configurators/omp.ts` (call wrapper) + `packages/cli/src/configurators/shared.ts` (new helper + data). + +## Out of Scope + +- Changing the source template format (stays as-is with `# Title` + `{{placeholders}}`) +- Touching other platforms' configurators +- Adding new commands + +## Technical Notes + +- `wrapWithCommandFrontmatter()` (line 275, shared.ts) is the Qoder/Claude-code variant — adds `name:` + `description:`. OMP doesn't need `name:` (filename is the name). +- `COMMAND_DESCRIPTIONS` (line 267, shared.ts) already has all 3 descriptions. +- OMP is `agentCapable: true` so `start` is filtered out — only `continue` and `finish-work` are emitted. +- User's hand-fix (reference): `trellis-continue.md` has `description:` only; `trellis-finish-work.md` has `description:` + `argument-hint: [task-name]`. diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json new file mode 100644 index 000000000..69cbae636 --- /dev/null +++ b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json @@ -0,0 +1,26 @@ +{ + "id": "omp-configurator-missing-command-frontmatter", + "name": "omp-configurator-missing-command-frontmatter", + "title": "OMP configurator missing command frontmatter", + "description": "", + "status": "completed", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "VinciWu557", + "assignee": "VinciWu557", + "createdAt": "2026-06-03", + "completedAt": "2026-06-03", + "branch": null, + "base_branch": "feat/support-oh-my-pi", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file From d8ded76e02b856c4f2bc7be354fcaf462a672bac Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Wed, 3 Jun 2026 15:15:27 +0800 Subject: [PATCH 14/19] chore: record journal --- .trellis/workspace/VinciWu557/index.md | 4 +-- .trellis/workspace/VinciWu557/journal-1.md | 33 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.trellis/workspace/VinciWu557/index.md b/.trellis/workspace/VinciWu557/index.md index 700598082..4ba89aac1 100644 --- a/.trellis/workspace/VinciWu557/index.md +++ b/.trellis/workspace/VinciWu557/index.md @@ -2,5 +2,5 @@ <!-- @@@auto:current-status --> - **Active File**: `journal-1.md` -- **Total Sessions**: 3 -- **Last Active**: 2026-05-26 \ No newline at end of file +- **Total Sessions**: 4 +- **Last Active**: 2026-06-03 \ No newline at end of file diff --git a/.trellis/workspace/VinciWu557/journal-1.md b/.trellis/workspace/VinciWu557/journal-1.md index 865db156d..c5ad231e3 100644 --- a/.trellis/workspace/VinciWu557/journal-1.md +++ b/.trellis/workspace/VinciWu557/journal-1.md @@ -104,3 +104,36 @@ Upgraded .omp/extensions/trellis/index.ts: session_start now invokes get_context ### Next Steps - None - task complete + + +## Session 4: Fix OMP command frontmatter generation + +**Date**: 2026-06-03 +**Task**: Fix OMP command frontmatter generation +**Branch**: `feat/support-oh-my-pi` + +### Summary + +Added wrapWithOmpFrontmatter() to shared.ts and wired it into omp.ts configurator. OMP commands now generate with proper YAML frontmatter (description + optional argument-hint). Updated platform-integration spec to include OMP in command format table and agent-capable list. Added 7 unit/integration tests. + +### Main Changes + +(Add details) + +### Git Commits + +| Hash | Message | +|------|---------| +| `3245bda` | (see git log) | + +### Testing + +- [OK] (Add test results) + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete From 48e9c6b4bbb0dadeeedde38267c138d9955aed00 Mon Sep 17 00:00:00 2001 From: taosu <taosu@mindfold.ai> Date: Sun, 7 Jun 2026 23:44:01 +0800 Subject: [PATCH 15/19] chore(omp): drop personal task/journal pollution + align dogfood skill name (#307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reviewer cleanups on top of the Oh My Pi platform work to ready the PR for merge review: 1. Remove personal Trellis dogfooding artifacts that crept into the diff while the platform was being developed. None of these belong in a reviewable PR — they leak the author's local workspace state into every reader's checkout: - `.trellis/workspace/VinciWu557/index.md` + `journal-1.md` - `.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/*` - `.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/*` - `.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/*` 2. Rename the dogfooded `.omp/skills/trellis-spec-bootstarp/` → `trellis-spec-bootstrap/` and fix the inner `name:` frontmatter field. This anticipates the bundled-skills source rename in #296 so that once #296 merges, the dogfooded `.omp/` tree matches what `trellis update --omp` regenerates. Until #296 lands, `trellis init --omp` on the PR HEAD still produces the typoed name because the source template hasn't been renamed yet — that is intentional and self-corrects on the next dogfood pass after #296. A `git merge origin/main` is still required before merge because main has moved on since this branch was opened (PR #324 + the historical-manifest restoration in #296). I left that conflict resolution to the maintainer at merge time rather than rebasing the contributor's commit history. Build / verification on the PR worktree: - `pnpm typecheck` clean - `pnpm test` 1049/1049 passing - `trellis init --omp --skip-existing -y` smoke test produces a valid `.omp/` tree --- .../SKILL.md | 2 +- .../references/mcp-setup.md | 0 .../references/repository-analysis.md | 0 .../references/spec-task-planning.md | 0 .../references/spec-writing.md | 0 .../check.jsonl | 5 - .../implement.jsonl | 7 - .../prd.md | 278 ------------------ .../task.json | 26 -- .../check.jsonl | 1 - .../implement.jsonl | 1 - .../prd.md | 138 --------- .../task.json | 26 -- .../check.jsonl | 2 - .../implement.jsonl | 2 - .../prd.md | 42 --- .../task.json | 26 -- .trellis/workspace/VinciWu557/index.md | 6 - .trellis/workspace/VinciWu557/journal-1.md | 139 --------- README.md | 2 +- README_CN.md | 2 +- 21 files changed, 3 insertions(+), 702 deletions(-) rename .omp/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/SKILL.md (98%) rename .omp/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/mcp-setup.md (100%) rename .omp/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/repository-analysis.md (100%) rename .omp/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/spec-task-planning.md (100%) rename .omp/skills/{trellis-spec-bootstarp => trellis-spec-bootstrap}/references/spec-writing.md (100%) delete mode 100644 .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md delete mode 100644 .trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json delete mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md delete mode 100644 .trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json delete mode 100644 .trellis/workspace/VinciWu557/index.md delete mode 100644 .trellis/workspace/VinciWu557/journal-1.md diff --git a/.omp/skills/trellis-spec-bootstarp/SKILL.md b/.omp/skills/trellis-spec-bootstrap/SKILL.md similarity index 98% rename from .omp/skills/trellis-spec-bootstarp/SKILL.md rename to .omp/skills/trellis-spec-bootstrap/SKILL.md index 2f7c7ad8f..af230a06b 100644 --- a/.omp/skills/trellis-spec-bootstarp/SKILL.md +++ b/.omp/skills/trellis-spec-bootstrap/SKILL.md @@ -1,5 +1,5 @@ --- -name: trellis-spec-bootstarp +name: trellis-spec-bootstrap description: "Bootstrap project-specific Trellis coding specs with a platform-neutral single-agent workflow. Use when creating or refreshing .trellis/spec guidelines, analyzing a codebase with GitNexus, ABCoder, or source inspection, decomposing package/layer spec work, and writing real codebase-backed spec docs without placeholder text." --- diff --git a/.omp/skills/trellis-spec-bootstarp/references/mcp-setup.md b/.omp/skills/trellis-spec-bootstrap/references/mcp-setup.md similarity index 100% rename from .omp/skills/trellis-spec-bootstarp/references/mcp-setup.md rename to .omp/skills/trellis-spec-bootstrap/references/mcp-setup.md diff --git a/.omp/skills/trellis-spec-bootstarp/references/repository-analysis.md b/.omp/skills/trellis-spec-bootstrap/references/repository-analysis.md similarity index 100% rename from .omp/skills/trellis-spec-bootstarp/references/repository-analysis.md rename to .omp/skills/trellis-spec-bootstrap/references/repository-analysis.md diff --git a/.omp/skills/trellis-spec-bootstarp/references/spec-task-planning.md b/.omp/skills/trellis-spec-bootstrap/references/spec-task-planning.md similarity index 100% rename from .omp/skills/trellis-spec-bootstarp/references/spec-task-planning.md rename to .omp/skills/trellis-spec-bootstrap/references/spec-task-planning.md diff --git a/.omp/skills/trellis-spec-bootstarp/references/spec-writing.md b/.omp/skills/trellis-spec-bootstrap/references/spec-writing.md similarity index 100% rename from .omp/skills/trellis-spec-bootstarp/references/spec-writing.md rename to .omp/skills/trellis-spec-bootstrap/references/spec-writing.md diff --git a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl deleted file mode 100644 index c4533637f..000000000 --- a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/check.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"_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."} -{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform integration patterns - verify consistency"} -{"file": ".trellis/spec/cli/backend/error-handling.md", "reason": "Error handling conventions for quality check"} -{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Unit test conventions for verification"} -{"file": ".trellis/spec/cli/unit-test/index.md", "reason": "Unit test overview for verification"} diff --git a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl deleted file mode 100644 index ce49f2c2e..000000000 --- a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/implement.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"_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."} -{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform integration patterns for adding new AI_TOOLS entry"} -{"file": ".trellis/spec/cli/backend/index.md", "reason": "CLI backend architecture overview"} -{"file": ".trellis/spec/cli/backend/directory-structure.md", "reason": "CLI directory structure conventions"} -{"file": ".trellis/spec/guides/cross-platform-thinking-guide.md", "reason": "Cross-platform thinking patterns for new platform support"} -{"file": ".trellis/tasks/05-22-add-omp-platform-support-to-trellis/research/omp-capabilities.md", "reason": "OMP platform capability details"} -{"file": ".trellis/tasks/05-22-add-omp-platform-support-to-trellis/research/OMP.md", "reason": "OMP integration design plan"} diff --git a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md deleted file mode 100644 index c65771f82..000000000 --- a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/prd.md +++ /dev/null @@ -1,278 +0,0 @@ -# PRD: Add OMP (Oh My Pi) Platform Support to Trellis - -## 1. 概述 - -在 Trellis CLI 中新增 `omp` 作为第 15 个 AI 编程平台。用户执行 `trellis init --omp` 时,CLI 在项目根生成 `.omp/` 下的所有必要文件,使 OMP 能通过原生 provider 自动发现并加载 Trellis 的工作流系统。 - -OMP 是 Pi 的进化版本,拥有更先进的 ExtensionAPI、原生 task tool、多源能力发现系统。OMP 原生覆盖了 Pi 插件中最复杂的部分(sub-agent 派生、bash 上下文注入、Agent 定义加载),使 Trellis 集成代码量相比 Pi 减少约 65%(~1173 行 → ~400 行)。 - -OMP extension(`.omp/extensions/trellis/index.ts`)需实现约 280 行 TypeScript,核心组件: - -- `resolveActiveTaskStatus()` — 读取 `.trellis/.runtime/sessions/` 解析活跃任务状态 -- `buildTaskContext()` — 读取 prd.md + info.md + JSONL 引用的 spec/research 文件 -- `buildSessionOverview()` — spawn `python3 get_context.py --mode session-overview`,5s 超时,失败回退静态文本(必须使用 `--mode session-overview` 以满足 spec Cross-platform consistency invariant 的 parity 要求) -- `TurnContextCache` — 1.5s TTL 防止同 turn 的 `input` + `before_agent_start` 双重 spawn -- `before_agent_start` handler — 注入 workflow-state + task context + session overview -- `input` handler — per-turn 刷新 workflow-state + session overview - -## 2. 背景 - -Trellis 已支持 14 个 AI 编程平台(Claude Code、Cursor、Codex、Gemini、Pi 等)。每个平台通过统一的 `AI_TOOLS` 注册表 + `configurators/` 调度机制接入。 - -**OMP vs Pi 架构差异决定插件策略**: - -| | Pi | OMP | -|---|---|---| -| Extension 运行位置 | 独立 wrapper 进程 | OMP 进程内 | -| Sub-agent 机制 | 插件自己 spawn `pi --mode text`(~500 行) | OMP 原生 task tool | -| 上下文注入方式 | `systemPrompt` 字段直接拼接 | `customType` custom message | -| 跨进程上下文 | 需要 TRELLIS_CONTEXT_ID 环境变量 + tool_call 拦截 | 单进程内文件系统直接读,不需要 | - -这些差异意味着移植 Pi 逻辑时要**简化而非照搬**:context key 管理链路(`resolveContextKey` / `adoptExistingContextKey` / `createProcessContextKey`)全部可省略,sub-agent 工具注册和 bash 拦截也不需要。但核心的上下文注入逻辑(任务内容 + session overview)必须保留,不能因 OMP 的便利性而省略。 - -相关规范: -- `.trellis/spec/cli/backend/platform-integration.md` — 平台接入架构与检查清单 - -## 3. 目标与范围 - -### 目标 -- `trellis init --omp` 生成正确的 `.omp/` 目录结构(commands、skills、agents、extension) -- `trellis update` 正确追踪 OMP 产物文件 -- OMP 启动后自动发现 Trellis extension 并加载所有能力 -- AI 在每轮 turn 获得 `<workflow-state>` breadcrumb,根据阶段触发对应 skill -- **AI 获得当前任务上下文**(prd.md + info.md + jsonl spec context),通过 `before_agent_start` 以 custom message 注入 -- **AI 获得完整 session overview**(`get_context.py` 输出),包含开发者信息、git 分支、活跃任务摘要 -- **添加 `input` handler**,确保 per-turn 注入的 breadcrumb 和 session overview 在长对话中保持新鲜 -- AI 通过 OMP 原生 `task` tool 调用 `trellis-implement` / `trellis-check` / `trellis-research` 子 agent - -### 非目标 (MVP) -- TTSR 规则(流式拦截工作流违规) -- 自定义 slash command(如 `/trellis:status`) -- `tool_result` 后处理(截断、注入链接) -- `context` 事件 handler(Pi 的 passthrough 本身是空操作,无需移植) -- `tool_call` 事件 handler(OMP 单进程内不需要 TRELLIS_CONTEXT_ID 环境变量注入) -- Sub-agent 工具注册 `registerTool("subagent", ...)`(OMP 原生 task tool 覆盖) -- Agent 定义加载(`.pi/agents/*.md`)(OMP skills/agents 自动发现覆盖) - -## 4. 关键设计决策 - -### 4.1 不生成 `settings.json` - -OMP 的 native provider 在启动时自动扫描 `.omp/` 下的标准子目录(`extensions/`、`commands/`、`skills/`、`agents/`),无需任何显式配置。这与 Pi 不同,Pi 需要在 `settings.json` 中显式声明所有能力路径和 npm 包依赖。 - -### 4.2 Extension 核心功能 - -OMP extension 实现以下注入能力(无需子 agent 调度和 bash 拦截,OMP 原生 task tool 覆盖): - -| 功能 | Pi extension (1173 行) | OMP extension (~400 行) | -|---|---|---| -| Workflow-state 面包屑 | before_agent_start + input | before_agent_start + input | -| 任务上下文(prd/info/jsonl) | before_agent_start systemPrompt 拼接 | before_agent_start custom message | -| Session overview | get_context.py 子进程 | get_context.py 子进程 | -| Sub-agent 调度 | 500+ 行 spawn 逻辑 | OMP task tool 原生处理 | -| Bash 上下文注入 | tool_call 拦截 TRELLIS_CONTEXT_ID | 不需要(单进程) | -| Context key 管理 | resolveContextKey + adoptExistingContextKey 链路 | 不需要(单进程) | - -与 Pi 的关键简化: -- **不需要跨进程 context key**:OMP extension 运行在 OMP 进程内,直接通过文件系统读取 session 文件获取当前任务,无需 `TRELLIS_CONTEXT_ID` 环境变量传递链路 -- **不需要 sub-agent 工具注册**:OMP 原生 `task` tool 覆盖 sub-agent 派生,不需要 `registerTool("subagent")` + `runPi()` spawn 逻辑 -- **注入方式改为 custom message**:OMP ExtensionAPI 不支持 `systemPrompt` 直接拼接,改为 `customType` custom message 注入,对模型行为的引导效果相当 - -### 4.3 Agent 不使用 pull-based prelude - -OMP 是真正的 class-1 平台:task tool 子 session 会重新加载 extensions,`before_agent_start` 自动触发并注入 breadcrumb。不需要 class-2 平台的 pull-based prelude 补偿机制。 - -### 4.4 Agent 工具名使用 OMP 原生命名 - -| Pi 工具名 | OMP 工具名 | -|---|---| -| Read, Write, Edit, Bash | read, write, edit, bash | -| Glob, Grep | find, search | -| (不存在) | ast_grep, lsp | - -### 4.5 不生成 `start` 命令 - -`agentCapable: true` → `filterCommands()` 自动过滤 `start`。Extension 在 session 启动时自动注入 workflow 上下文。 - -## 5. 实现计划 - -分两阶段执行:**Phase 1** 先手动生成 `.omp/` 产物文件用于 OMP 端验证,验证通过后 **Phase 2** 再适配 Trellis CLI 使其通过 `trellis init --omp` 自动化生成。 - -### Phase 1: 手动生成 .omp/ 产物(先行验证) - -直接在 Trellis 项目根创建 `.omp/` 目录及所有文件,用于在 OMP 中实际测试: - -1. **commands** — 2 个斜杠命令 -2. **skills** — 5 个技能 + 2 个多文件技能 -3. **agents** — 3 个 agent 定义 -4. **extensions** — 1 个 extension 入口 - -目标:确认 OMP 能正确加载 extension、技能被发现、agent 可通过 task tool 调用、workflow-state breadcrumb 注入生效。 - -验证通过后进入 Phase 2。 - -### Phase 2: 适配 Trellis CLI - -#### 5.2.1 `types/ai-tools.ts` — 新增 OMP 平台注册 - -```typescript -omp: { - name: "Oh My Pi", - templateDirs: ["common", "omp"], - configDir: ".omp", - cliFlag: "omp", - defaultChecked: false, - hasPythonHooks: false, - templateContext: { - cmdRefPrefix: "/trellis:", - executorAI: "Bash scripts or Task calls", - userActionLabel: "Slash commands", - agentCapable: true, // class-1: extension 注入 context - hasHooks: true, - cliFlag: "omp", - }, -}, -``` - -`AITool`、`TemplateDir`、`CliFlag` 联合类型各增加 `"omp"` 字面量。 - -#### 5.2.2 `templates/omp/` — 新建模板目录 - -将 Phase 1 验证通过的产物文件搬入模板目录: - -``` -templates/omp/ -├── index.ts # 导出 getAllAgents() + getExtensionTemplate() -├── agents/ -│ ├── trellis-implement.md # OMP 原生工具名, 无 pull-based prelude -│ ├── trellis-check.md # OMP 原生工具名, 无 pull-based prelude -│ └── trellis-research.md # 含 web_search 工具 -└── extensions/ - └── trellis/ - └── index.ts.txt # ExtensionAPI 模板源(.txt 后缀防止 tsc 编译,与 Pi 一致) -``` - -> **重要约定**:Extension 模板文件必须使用 `.ts.txt` 后缀(参照 Pi 的 `index.ts.txt`),原因: -> 1. 防止 `tsc` 在 production build 时将其编译为 `.js` 并输出到 `dist/templates/omp/extensions/` -> 2. 与 Pi 模式保持一致(spec `platform-integration.md` Step 4 "TypeScript extension pattern" 明确要求 `.ts.txt`) -> 3. `readTemplate("extensions/trellis/index.ts.txt")` 读取,configurator 写入时去掉 `.txt` 后缀 - -#### 5.2.3 `configurators/omp.ts` — 新建配置器 - -``` -export configureOmp(cwd): 写入 commands、skills、agents、extension -export collectOmpTemplates(): 返回所有产物文件 Map (供 trellis update 哈希比对) -``` - -参照 Pi 配置器,简化点: -- 无 `settings.json` -- Agent 不调用 `applyPullBasedPreludeMarkdown` -- 命令输出到 `.omp/commands/`(非 `.pi/prompts/`) - -> **关键规则:`collectOmpTemplates()` 必须对 extension 模板应用与 `configureOmp()` 相同的内容变换**。 -> 具体而言:`configureOmp()` 写入 extension 时调用 `replacePythonCommandLiterals(getExtensionTemplate())`,`collectOmpTemplates()` 返回同一文件内容时也必须包裹 `replacePythonCommandLiterals()`。 -> 否则 Windows 上 `trellis update` 每次运行都会误报 extension 文件变更(因为 `python3` vs `python` 不一致导致哈希不匹配)。 -> 参见 spec `platform-integration.md` → Common Mistakes → "Template placeholder not resolved in collectTemplates"。 - -#### 5.2.4 `configurators/index.ts` — 注册 OMP - -```typescript -PLATFORM_FUNCTIONS.omp = { - configure: configureOmp, - collectTemplates: () => collectOmpTemplates(), -}; -``` - -#### 5.2.5 CLI + InitOptions - -- `cli/index.ts`:添加 `--omp` flag -- `commands/init.ts`:`InitOptions` 添加 `omp?: boolean` - -#### 5.2.6 Extension 核心逻辑 (`templates/omp/extensions/trellis/index.ts`) - -``` -session_start → 发现项目根 (.trellis 存在) → 通知 UI -before_agent_start → 注入 workflow-state breadcrumb + task context (prd/info/jsonl) + session overview -input → per-turn 刷新 workflow-state + session overview (TurnContextCache 1.5s TTL) -``` - -约 280 行 TypeScript。依赖关系:`ExtensionAPI` from `@oh-my-pi/pi-coding-agent`。 - -## 6. 产物文件完整清单 - -``` -.omp/ -├── commands/ -│ ├── trellis-continue.md -│ └── trellis-finish-work.md -├── skills/ -│ ├── trellis-brainstorm/SKILL.md -│ ├── trellis-before-dev/SKILL.md -│ ├── trellis-check/SKILL.md -│ ├── trellis-break-loop/SKILL.md -│ ├── trellis-update-spec/SKILL.md -│ ├── trellis-meta/... (多文件) -│ └── trellis-spec-bootstarp/... (多文件) -├── agents/ -│ ├── trellis-implement.md -│ ├── trellis-check.md -│ └── trellis-research.md -└── extensions/ - └── trellis/ - └── index.ts -``` - -## 7. 风险与缓解 - -| 风险 | 缓解 | -|---|---| -| `findAllNearestProjectConfigDirs` walk-up 行为导致 `.omp/` 在错误层级生成 | 确保 init 时 cwd 为 workspace 根 | -| 用户已禁用多个 provider(opencode, codex, gemini, cursor),但 native provider 未被禁用 | 验证 native provider 仍加载 `.omp/` 内容 | -| `skills.enablePiUser` / `enablePiProject` 遗留开关可能 gate native skill source | 文档提醒用户检查此项 | -| `ast_grep` 工具名是否与 OMP 实际注册名一致 | 在真实 OMP 环境验证 agent 工具列表 | -| Agent 发现是每次执行时重新扫描,模板变更可能不生效 | 需 OMP session 重载才能捕获 agent 变更 | - -## 8. 验证计划 - -### Phase 1 验证 (OMP 端) -1. 手动创建 `.omp/` 目录结构,检查产物文件内容格式正确 -2. 启动 OMP,确认 extension 加载且技能被发现 -3. AI 获得 `<workflow-state>` breadcrumb,能根据阶段触发对应 skill -4. AI 能通过 `task` tool 调用 `trellis-implement` / `trellis-check` / `trellis-research` - -### Phase 2 验证 (CLI 端) -5. `trellis init --omp` 生成与 Phase 1 一致的 `.omp/` 目录结构 -6. `trellis update` 正确追踪 OMP 产物文件 -7. 回归验证:Phase 1 中通过的 OMP 端测试在 `trellis init --omp` 产物上再次通过 - -## 9. 工作量估算 - -### Phase 1 -| 组件 | 内容 | 复杂度 | -|---|---|---| -| `.omp/commands/` | 2 个命令文件 | 低 | -| `.omp/skills/` | 5 个技能 + 2 个多文件技能 | 低 | -| `.omp/agents/` | 3 个 agent 定义 | 低 | -| `.omp/extensions/trellis/index.ts` | extension 入口 | 中 | -| **Phase 1 总计** | — | — | - -### Phase 2 -| 组件 | 代码量 | 复杂度 | -|---|---|---| -| `types/ai-tools.ts` 修改 | 3 行 + 1 条目 | 低 | -| `configurators/omp.ts` | ~80 行 | 中 | -| `templates/omp/` (从 Phase 1 产物搬入) | 6+ 文件 | 低 | -| `templates/omp/index.ts` | ~15 行 | 低 | -| `configurators/index.ts` | 4 行 | 低 | -| CLI + InitOptions | ~10 行 | 低 | -| **Phase 2 总计** | **~110 行新代码** + 模板搬运 | — | - -## 10. 开发命令 - -```bash -# 构建(在 packages/cli 下执行) -cd packages/cli && pnpm run build - -# 一次性测试(回到仓库根目录执行,不影响已安装的 trellis) -cd ../.. && node ./packages/cli/bin/trellis.js init --omp -``` diff --git a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json b/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json deleted file mode 100644 index 530376c32..000000000 --- a/.trellis/tasks/archive/2026-05/05-22-add-omp-platform-support-to-trellis/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "add-omp-platform-support-to-trellis", - "name": "add-omp-platform-support-to-trellis", - "title": "Add OMP platform support to Trellis", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "--name", - "assignee": "--name", - "createdAt": "2026-05-22", - "completedAt": "2026-05-25", - "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/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl deleted file mode 100644 index 9dd3234a2..000000000 --- a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/check.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"_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/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl deleted file mode 100644 index 9dd3234a2..000000000 --- a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/implement.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"_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/archive/2026-05/05-26-omp-trellis-context-injection/prd.md b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md deleted file mode 100644 index 595745a3f..000000000 --- a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/prd.md +++ /dev/null @@ -1,138 +0,0 @@ -# OMP Trellis Extension: 补齐上下文注入和 SubAgent 精确注入 - -## Goal - -升级 `.omp/extensions/trellis/index.ts`,补齐两个核心能力: -1. Session Start 富注入 — 提供项目全局地图(git/spec/task/identity) -2. SubAgent 精确注入 — 根据 agent 类型选择性注入 task context,减少 token 浪费 - -保留 OMP 原生 Extension API 的架构优势(进程内执行、类型安全、`context` 事件)。 - -## Requirements - -### R1: Session Start 富注入 - -在 `session_start` 事件中通过 `pi.sendMessage()` 注入以下静态上下文(`display: false`): - -| 内容 | 来源 | 预估大小 | -|------|------|----------| -| Git branch + dirty count + 最近 3 commits | `spawnSync("git", ...)` | ~300B | -| Spec 索引 | `readdirSync(".trellis/spec/")` 递归一层 | ~500B | -| Active task 摘要 | 复用现有 `resolveActiveTaskStatus()` | ~100B | -| Developer identity | 读取 `.trellis/workspace/*/index.md` 匹配 git user | ~50B | - -总计 ~1-2KB,格式为 `<session-context>` XML tag。 - -**不注入**:workflow.md 内容、task context 详情、spec 文件正文(这些由 `before_agent_start` 每轮负责)。 - -**Compaction 后不重新注入** — `before_agent_start` 的 workflow state 已足够保底,session start 信息在 compaction 后可能已过时。 - -### R2: SubAgent 精确注入 - -Sub-agent session 会重新加载 extension。利用 `PI_BLOCKED_AGENT` env var 区分 agent 类型: - -| Agent 类型 | `PI_BLOCKED_AGENT` 值 | 注入内容 | -|-----------|----------------------|----------| -| `trellis-implement` | `"trellis-implement"` | `prd.md` + `info.md` + `implement.jsonl` 引用的文件 | -| `trellis-check` | `"trellis-check"` | `prd.md` + `info.md` + `check.jsonl` 引用的文件 | -| `trellis-research` | `"trellis-research"` | 仅 `prd.md` + `info.md` | -| 其他 / 主 session | 未设置或不匹配 | 全量注入(当前行为) | - -**注入时机分工(方案 C)**: -- `session_start` → 重量级 task context 文件注入(一次性) -- `before_agent_start` → 仅 workflow state 刷新(轻量,每轮) - -## Decision (ADR-lite) - -**Context**: 当前 extension 对主 session 和 sub-agent 使用完全相同的注入逻辑,导致 sub-agent 收到冗余信息(如 check agent 收到 implement.jsonl 的文件),且 session start 缺乏全局视图。 - -**Decision**: -- Session start 做"全局地图"(静态信息,一次性) -- before_agent_start 做"当前导航"(workflow state,轻量刷新) -- Sub-agent 中 session_start 做精确 task context 注入 -- 不引入配置层(OMP 已有 `disabledExtensions` 机制) -- Compaction 后不重新注入(before_agent_start 保底) - -**Consequences**: -- Sub-agent token 消耗降低(仅注入相关 jsonl) -- AI 首轮即有项目全局认知 -- 无新配置文件/新依赖 -- 若未来需要更细粒度控制,可通过 env var 扩展 - -## Acceptance Criteria - -- [ ] 主 session 首轮 AI 回复前,session context 已注入(含 git branch/dirty/commits + spec 索引 + task 摘要 + developer name) -- [ ] `trellis-implement` sub-agent 只收到 implement.jsonl 引用的文件,不含 check.jsonl -- [ ] `trellis-check` sub-agent 只收到 check.jsonl 引用的文件,不含 implement.jsonl -- [ ] `trellis-research` sub-agent 只收到 prd.md + info.md -- [ ] 主 session 的 before_agent_start 行为不变(backward compatible) -- [ ] 无 TypeScript 编译错误 -- [ ] session_start 中的 git/spec 操作耗时 <500ms -- [ ] `pnpm test` 通过(含更新后的 omp.test.ts marker 断言) - -## Definition of Done - -- TypeScript 编译通过 -- `pnpm test` 通过 -- 手动验证三类 sub-agent 的 context 差异 -- 代码风格与现有 extension 一致 -- 不引入外部依赖(仅 Node.js stdlib + OMP Extension API) - -## Out of Scope - -- 配置层 / settings.json(不需要,平台已有 disable 机制) -- Windows 路径规范化 -- 多平台检测 -- 新增 commands / skills -- 修改 agent `.md` 文件(增厚 prompt 为独立任务) -- 修改 `.trellis/scripts/` Python 脚本 -- `context` 事件裁剪优化(作为后续迭代) -- Compaction 后重新注入 - -## Technical Notes - -### 文件影响范围(3 个文件) - -| 文件 | 性质 | -|------|------| -| `.omp/extensions/trellis/index.ts` | 主改动 | -| `packages/cli/src/templates/omp/extensions/trellis/index.ts.txt` | 模板同步(内容 = 上面文件) | -| `packages/cli/test/templates/omp.test.ts` | 补充 marker 断言 | - -### 不需要改动的文件 - -- `configurators/omp.ts` — 通过 `getExtensionTemplate()` 自动获得新内容 -- `templates/omp/index.ts` — 导出函数签名不变 -- `types/ai-tools.ts` — 平台注册信息不变 -- `.trellis/scripts/common/cli_adapter.py` — 路径解析不变 -- `.trellis/scripts/common/task_store.py` — subagent config 不变 -- `regression.test.ts` — 现有断言测平台注册,不受内容变化影响 - -### 实现要点 - -1. **Session start 信息获取**: - - `spawnSync("git", ["branch", "--show-current"])` → branch name - - `spawnSync("git", ["status", "--porcelain"])` → dirty count - - `spawnSync("git", ["log", "--oneline", "-3"])` → recent commits - - `readdirSync(".trellis/spec/")` → spec package list, 每个 package 下一层列 layers - - 复用 `resolveActiveTaskStatus()` → task title + status - - 读取 `.trellis/workspace/` 下与 `git config user.name` 匹配的目录 - -2. **Sub-agent 检测**: - - `process.env.PI_BLOCKED_AGENT` → agent name(子 session 中自动设置) - - 存在此 env var → 当前是 sub-agent session - -3. **Task context 拆分**: - - 重构 `buildTaskContext()` 为 `buildTaskContext(projectRoot, taskDir, agentType?)` - - `agentType` 未传时保持全量行为(backward compatible) - - `agentType` 传入时只读取对应 jsonl - -4. **注入方式**: - - session_start: `pi.sendMessage({ customType: "trellis-session-context", content, display: false })` - - sub-agent task context: 同样在 session_start 中注入(一次性) - - before_agent_start: 保持仅 workflow state 注入(sub-agent 中也生效) - -5. **模板同步规则**: - - `.omp/extensions/trellis/index.ts` 与 `packages/cli/src/templates/omp/extensions/trellis/index.ts.txt` 内容完全相同 - - `replacePythonCommandLiterals()` 由 configurator 在 `trellis init --omp` 时自动应用 - - 模板源文件中直接写 `python3` diff --git a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json b/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json deleted file mode 100644 index 555fb080c..000000000 --- a/.trellis/tasks/archive/2026-05/05-26-omp-trellis-context-injection/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "omp-trellis-context-injection", - "name": "omp-trellis-context-injection", - "title": "OMP Trellis Extension: 补齐上下文注入、配置层和 SubAgent 精确注入", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "VinciWu557", - "assignee": "VinciWu557", - "createdAt": "2026-05-26", - "completedAt": "2026-05-26", - "branch": null, - "base_branch": "feat/support-oh-my-pi", - "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/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl deleted file mode 100644 index 154b5eefb..000000000 --- a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/check.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Verify command format matches OMP spec and doesn't break collectTemplates hash comparison"} -{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Verify test follows project conventions"} diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl deleted file mode 100644 index 1a570192a..000000000 --- a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/implement.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"file": ".trellis/spec/cli/backend/platform-integration.md", "reason": "Platform configurator architecture, command format table, wrapWithCommandFrontmatter pattern"} -{"file": ".trellis/spec/cli/unit-test/conventions.md", "reason": "Unit test conventions for writing configurator tests"} diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md deleted file mode 100644 index 66b62e0fa..000000000 --- a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/prd.md +++ /dev/null @@ -1,42 +0,0 @@ -# OMP Configurator Missing Command Frontmatter - -## Goal - -`configureOmp()` and `collectOmpTemplates()` write command files without YAML frontmatter. OMP platform requires `---\ndescription: ...\n---` (and optionally `argument-hint:`) at the top for the `/` command picker to work. Fix the configurator so `trellis init`/`trellis update` produces correctly formatted OMP commands. - -## Requirements - -- Commands written to `.omp/commands/` must include YAML frontmatter with `description` field -- Commands that accept positional args should include `argument-hint` field (e.g. `finish-work` has `[task-name]`) -- The `# Title` H1 heading in source templates must be stripped (frontmatter replaces its role) -- The existing `COMMAND_DESCRIPTIONS` map in `shared.ts` should be the source of truth for `description` values -- Argument hints need a new data source (small map or extension of existing structure) - -## Acceptance Criteria - -- [ ] `collectOmpTemplates()` output for each command starts with valid YAML frontmatter -- [ ] `configureOmp()` writes commands with matching frontmatter -- [ ] Generated output matches the format user hand-fixed: `---\ndescription: ...\n[argument-hint: ...]\n---\n\n<body without H1>` -- [ ] Existing tests pass; new unit test covers OMP command frontmatter generation - -## Technical Approach - -Add an OMP-specific command wrapper (analogous to `wrapWithCommandFrontmatter` used by Qoder). Two sub-decisions: - -1. **Strip H1**: Source templates start with `# Title\n\n<body>`. OMP format uses frontmatter instead of H1. Strip the first `# ...` line + trailing blank when wrapping. -2. **Argument hints**: Add an `ARGUMENT_HINTS` map (or extend `COMMAND_DESCRIPTIONS` to a richer object). Only `finish-work` currently needs one (`[task-name]`). - -Implementation site: `packages/cli/src/configurators/omp.ts` (call wrapper) + `packages/cli/src/configurators/shared.ts` (new helper + data). - -## Out of Scope - -- Changing the source template format (stays as-is with `# Title` + `{{placeholders}}`) -- Touching other platforms' configurators -- Adding new commands - -## Technical Notes - -- `wrapWithCommandFrontmatter()` (line 275, shared.ts) is the Qoder/Claude-code variant — adds `name:` + `description:`. OMP doesn't need `name:` (filename is the name). -- `COMMAND_DESCRIPTIONS` (line 267, shared.ts) already has all 3 descriptions. -- OMP is `agentCapable: true` so `start` is filtered out — only `continue` and `finish-work` are emitted. -- User's hand-fix (reference): `trellis-continue.md` has `description:` only; `trellis-finish-work.md` has `description:` + `argument-hint: [task-name]`. diff --git a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json b/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json deleted file mode 100644 index 69cbae636..000000000 --- a/.trellis/tasks/archive/2026-06/06-03-omp-configurator-missing-command-frontmatter/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "omp-configurator-missing-command-frontmatter", - "name": "omp-configurator-missing-command-frontmatter", - "title": "OMP configurator missing command frontmatter", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "VinciWu557", - "assignee": "VinciWu557", - "createdAt": "2026-06-03", - "completedAt": "2026-06-03", - "branch": null, - "base_branch": "feat/support-oh-my-pi", - "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/workspace/VinciWu557/index.md b/.trellis/workspace/VinciWu557/index.md deleted file mode 100644 index 4ba89aac1..000000000 --- a/.trellis/workspace/VinciWu557/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# Developer Journal — VinciWu557 - -<!-- @@@auto:current-status --> -- **Active File**: `journal-1.md` -- **Total Sessions**: 4 -- **Last Active**: 2026-06-03 \ No newline at end of file diff --git a/.trellis/workspace/VinciWu557/journal-1.md b/.trellis/workspace/VinciWu557/journal-1.md deleted file mode 100644 index c5ad231e3..000000000 --- a/.trellis/workspace/VinciWu557/journal-1.md +++ /dev/null @@ -1,139 +0,0 @@ -# Journal - VinciWu557 (Part 1) - -> Started: 2026-05-25 - ---- - - - -## Session 1: Add Oh My Pi platform support - -**Date**: 2026-05-25 -**Task**: Add Oh My Pi platform support -**Branch**: `feat/support-oh-my-pi` - -### Summary - -Added Oh My Pi (omp) platform support to Trellis, implementing Phase 1 and Phase 2 of the integration. - -### Main Changes - -- Implemented omp platform adapter (Phase 1 + Phase 2) -- Added platform detection and configuration for Oh My Pi - -### Git Commits - -| Hash | Message | -|------|---------| -| `5fb8a25` | feat(omp): add Oh My Pi platform support (Phase 1 + Phase 2) | - -### Testing - -- [OK] Build passes - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete - - -## Session 2: Add Oh My Pi platform support - -**Date**: 2026-05-25 -**Task**: Add Oh My Pi platform support -**Branch**: `feat/support-oh-my-pi` - -### Summary - -Added Oh My Pi (omp) platform support to Trellis, implementing Phase 1 + Phase 2 platform adapter and configuration. - -### Main Changes - -(Add details) - -### Git Commits - -| Hash | Message | -|------|---------| -| `5fb8a25` | (see git log) | - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete - - -## Session 3: OMP extension: session-start rich injection + sub-agent precision - -**Date**: 2026-05-26 -**Task**: OMP extension: session-start rich injection + sub-agent precision -**Branch**: `feat/support-oh-my-pi` - -### Summary - -Upgraded .omp/extensions/trellis/index.ts: session_start now invokes get_context.py for full project map; sub-agents receive only their relevant jsonl files via PI_BLOCKED_AGENT detection; before_agent_start simplified to workflow-state only. Fixed dead buildSessionOverview calling non-existent script mode. All 1042 tests pass. - -### Main Changes - -(Add details) - -### Git Commits - -| Hash | Message | -|------|---------| -| `307234f` | (see git log) | - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete - - -## Session 4: Fix OMP command frontmatter generation - -**Date**: 2026-06-03 -**Task**: Fix OMP command frontmatter generation -**Branch**: `feat/support-oh-my-pi` - -### Summary - -Added wrapWithOmpFrontmatter() to shared.ts and wired it into omp.ts configurator. OMP commands now generate with proper YAML frontmatter (description + optional argument-hint). Updated platform-integration spec to include OMP in command format table and agent-capable list. Added 7 unit/integration tests. - -### Main Changes - -(Add details) - -### Git Commits - -| Hash | Message | -|------|---------| -| `3245bda` | (see git log) | - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete diff --git a/README.md b/README.md index 7a11abb48..5c1a0ad1a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ <p align="center"> <strong>An out-of-the-box engineering framework for AI coding.</strong><br/> -<sub>AI writes code fast, but every session it starts from scratch — no memory of your project, your conventions, or your team's requirements. Trellis persists specs, tasks, and memory into your repo, so any coding agent works to your engineering standards. Supports Claude Code, Cursor, OpenCode, Codex, Kiro, Kilo, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Droid, Pi Agent, Reasonix, and Oh My Pi.</sub> +<sub>AI writes code fast, but every session it starts from scratch — no memory of your project, your conventions, or your team's requirements. Trellis persists specs, tasks, and memory into your repo, so any coding agent works to your engineering standards.</sub> </p> <p align="center"> diff --git a/README_CN.md b/README_CN.md index df9a3edb1..422e1ade2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,7 @@ <p align="center"> <strong>开箱即用的 AI 编码工程化框架</strong><br/> -<sub>AI 写代码很快,但它每次会话都从零开始理解项目,记不住你的规范,也记不住团队级别的需求。Trellis 会把规范、任务、记忆沉淀进仓库,让任意 Coding Agent 都按你的工程标准来实践。支持 Claude Code、Cursor、OpenCode、Codex、Kiro、Kilo、Gemini CLI、Antigravity、Windsurf、Qoder、CodeBuddy、GitHub Copilot、Droid、Pi Agent、Reasonix 和 Oh My Pi。</sub> +<sub>AI 写代码很快,但它每次会话都从零开始理解项目,记不住你的规范,也记不住团队级别的需求。Trellis 会把规范、任务、记忆沉淀进仓库,让任意 Coding Agent 都按你的工程标准来实践。</sub> </p> <p align="center"> From 8c9f727ee0f49e8cc4dbfd309451935e233f5bf5 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Fri, 19 Jun 2026 18:11:12 +0800 Subject: [PATCH 16/19] =?UTF-8?q?fix(omp):=20resolve=20PR=20review=20block?= =?UTF-8?q?ers=20=E2=80=94=20reformat=20noise=20+=20docs=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocker 1: revert reformatting noise in cli/index.ts, init.ts, configurators/index.ts. Rebase onto main and re-apply only the omp registration lines (7 lines net, down from ~3900 lines of 3-space reindentation noise). Blocker 2: sync docs-site for the Oh My Pi platform. - start/install-and-first-task.mdx (en + zh): add --omp flag, bump platform count 16 -> 17, add Oh My Pi row to platform config table - advanced/multi-platform.mdx (en + zh): bump platform count, add Oh My Pi to capability matrix and platform list, add Oh My Pi section (extension-backed, like Pi Agent, no settings.json) - marketplace/workflows/native/workflow.md: mirror the bundled workflow.md Oh My Pi platform markers Submodule pointers updated to the synced docs-site and marketplace commits on the feat/support-oh-my-pi branches of their forks. --- docs-site | 2 +- marketplace | 2 +- packages/cli/src/cli/index.ts | 1 + packages/cli/src/commands/init.ts | 1 + packages/cli/src/configurators/index.ts | 5 +++++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs-site b/docs-site index dcf265210..b53d3ecf1 160000 --- a/docs-site +++ b/docs-site @@ -1 +1 @@ -Subproject commit dcf265210ba066cddaabb25aeff2598ec815e69c +Subproject commit b53d3ecf1e97b9ee8098443fefefaea5c645e4d4 diff --git a/marketplace b/marketplace index 2f2e30d14..c9a191832 160000 --- a/marketplace +++ b/marketplace @@ -1 +1 @@ -Subproject commit 2f2e30d1465fe3e2a4ea8cd107dab2a1070ef25b +Subproject commit c9a191832c807bdc1ea3180a800194c9af0cb3a0 diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index 53149386a..97a923fb5 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -85,6 +85,7 @@ program .option("--pi", "Include Pi Agent extension assets") .option("--reasonix", "Include Reasonix skills") .option("--zcode", "Include ZCode commands") + .option("--omp", "Include Oh My Pi extension assets") .option("--trae", "Include Trae IDE commands") .option( "--with-statusline", diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e84397884..54161c02e 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1021,6 +1021,7 @@ interface InitOptions { reasonix?: boolean; zcode?: boolean; trae?: boolean; + omp?: boolean; yes?: boolean; user?: string; force?: boolean; diff --git a/packages/cli/src/configurators/index.ts b/packages/cli/src/configurators/index.ts index 6be97d37c..76aab293a 100644 --- a/packages/cli/src/configurators/index.ts +++ b/packages/cli/src/configurators/index.ts @@ -35,6 +35,7 @@ import { configurePi, collectPiTemplates } from "./pi.js"; import { configureReasonix, collectReasonixTemplates } from "./reasonix.js"; import { configureZcode, collectZcodeTemplates } from "./zcode.js"; import { configureTrae } from "./trae.js"; +import { configureOmp, collectOmpTemplates } from "./omp.js"; // Shared utilities import { @@ -487,6 +488,10 @@ const PLATFORM_FUNCTIONS: Record<AITool, PlatformFunctions> = { return files; }, }, + omp: { + configure: configureOmp, + collectTemplates: () => collectOmpTemplates(), + }, }; // ============================================================================= From 9286facc54ea35542fa6f6eb824f090bd0cf3254 Mon Sep 17 00:00:00 2001 From: vinci <826296748@qq.com> Date: Fri, 26 Jun 2026 11:36:46 +0800 Subject: [PATCH 17/19] fix(omp): remove unused import + session-aware active task resolution - Remove unused resolveCommands import in shared.test.ts - Replace mtime-based session picking with context-key lookup - Derive TRELLIS_CONTEXT_ID from ctx.sessionManager.getSessionId() in session_start - Fall back to single-session file only when exactly one exists; refuse to guess with 2+ - Remove dead PI_SESSION_ID env check (OMP does not export it) --- .omp/extensions/trellis/index.ts | 85 +- .../omp/extensions/trellis/index.ts.txt | 85 +- .../test/commands/update.integration.test.ts | 2218 ++++++++--------- .../cli/test/configurators/shared.test.ts | 1 - packages/cli/test/templates/trellis.test.ts | 8 +- 5 files changed, 1249 insertions(+), 1148 deletions(-) diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts index e68db0806..e6615c816 100644 --- a/.omp/extensions/trellis/index.ts +++ b/.omp/extensions/trellis/index.ts @@ -1,7 +1,8 @@ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; // --------------------------------------------------------------------------- // Project root detection @@ -18,6 +19,36 @@ function findProjectRoot(startDir: string): string | null { return null; } +// --------------------------------------------------------------------------- +// Session identity helpers (mirrors Python _sanitize_key / _hash_value / _context_key) +// --------------------------------------------------------------------------- + +function sanitizeKey(raw: string): string { + const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, ""); + return safe ? safe.slice(0, 160) : ""; +} + +function hashValue(raw: string): string { + return createHash("sha256").update(raw).digest("hex").slice(0, 24); +} + +function buildContextKey(platformName: string, kind: string, value: string): string { + if (kind === "transcript") { + return `${platformName}_transcript_${hashValue(value)}`; + } + const safeValue = sanitizeKey(value); + return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`; +} + +function resolveContextKey(): string | null { + // 1. 显式覆盖 — Trellis hooks 启动子进程时设置 + const override = process.env.TRELLIS_CONTEXT_ID?.trim(); + if (override) { + return sanitizeKey(override) || hashValue(override); + } + return null; +} + // --------------------------------------------------------------------------- // Active task resolution // --------------------------------------------------------------------------- @@ -28,28 +59,38 @@ function resolveActiveTaskStatus( const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; - let sessionFiles: string[]; - try { - sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); - } catch { - return { status: "no_task", taskDir: null, taskTitle: null }; + // --- 通过 context key 解析 session 文件 --- + const contextKey = resolveContextKey(); + let sessionFilePath: string | null = null; + + if (contextKey) { + // 有身份标识:直接定位文件 + const candidate = join(sessionsDir, `${contextKey}.json`); + if (existsSync(candidate)) { + sessionFilePath = candidate; + } } - if (sessionFiles.length === 0) return { status: "no_task", taskDir: null, taskTitle: null }; - if (sessionFiles.length > 1) { - sessionFiles.sort((a, b) => { - const ma = statSync(join(sessionsDir, a)).mtimeMs; - const mb = statSync(join(sessionsDir, b)).mtimeMs; - return mb - ma; - }); + if (!sessionFilePath) { + // 单 session fallback:恰好 1 个文件时使用,否则拒绝猜测 + let sessionFiles: string[]; + try { + sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); + } catch { + return { status: "no_task", taskDir: null, taskTitle: null }; + } + if (sessionFiles.length === 1) { + sessionFilePath = join(sessionsDir, sessionFiles[0]); + } else { + // 0 或 ≥2 个文件且无身份标识:返回 no_task,不跨会话猜测 + return { status: "no_task", taskDir: null, taskTitle: null }; + } } - const sessionFile = sessionFiles[0]; + // --- 读取 session 数据 --- let sessionData: Record<string, unknown>; try { - sessionData = JSON.parse( - readFileSync(join(sessionsDir, sessionFile), "utf-8"), - ); + sessionData = JSON.parse(readFileSync(sessionFilePath, "utf-8")); } catch { return { status: "no_task", taskDir: null, taskTitle: null }; } @@ -276,6 +317,16 @@ export default function(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); + // Derive context key from OMP session identity and export it so + // resolveActiveTaskStatus() and spawned subprocesses (get_context.py, + // task.py) can locate the correct session runtime file. + if (!process.env.TRELLIS_CONTEXT_ID) { + const sessionId = (ctx.sessionManager as { getSessionId?: () => string }).getSessionId?.(); + if (sessionId) { + process.env.TRELLIS_CONTEXT_ID = buildContextKey("pi", "session", sessionId); + } + } + if (!projectRoot) return; if (isSubAgent) { diff --git a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt index e68db0806..e6615c816 100644 --- a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -1,7 +1,8 @@ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; // --------------------------------------------------------------------------- // Project root detection @@ -18,6 +19,36 @@ function findProjectRoot(startDir: string): string | null { return null; } +// --------------------------------------------------------------------------- +// Session identity helpers (mirrors Python _sanitize_key / _hash_value / _context_key) +// --------------------------------------------------------------------------- + +function sanitizeKey(raw: string): string { + const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, ""); + return safe ? safe.slice(0, 160) : ""; +} + +function hashValue(raw: string): string { + return createHash("sha256").update(raw).digest("hex").slice(0, 24); +} + +function buildContextKey(platformName: string, kind: string, value: string): string { + if (kind === "transcript") { + return `${platformName}_transcript_${hashValue(value)}`; + } + const safeValue = sanitizeKey(value); + return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`; +} + +function resolveContextKey(): string | null { + // 1. 显式覆盖 — Trellis hooks 启动子进程时设置 + const override = process.env.TRELLIS_CONTEXT_ID?.trim(); + if (override) { + return sanitizeKey(override) || hashValue(override); + } + return null; +} + // --------------------------------------------------------------------------- // Active task resolution // --------------------------------------------------------------------------- @@ -28,28 +59,38 @@ function resolveActiveTaskStatus( const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; - let sessionFiles: string[]; - try { - sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); - } catch { - return { status: "no_task", taskDir: null, taskTitle: null }; + // --- 通过 context key 解析 session 文件 --- + const contextKey = resolveContextKey(); + let sessionFilePath: string | null = null; + + if (contextKey) { + // 有身份标识:直接定位文件 + const candidate = join(sessionsDir, `${contextKey}.json`); + if (existsSync(candidate)) { + sessionFilePath = candidate; + } } - if (sessionFiles.length === 0) return { status: "no_task", taskDir: null, taskTitle: null }; - if (sessionFiles.length > 1) { - sessionFiles.sort((a, b) => { - const ma = statSync(join(sessionsDir, a)).mtimeMs; - const mb = statSync(join(sessionsDir, b)).mtimeMs; - return mb - ma; - }); + if (!sessionFilePath) { + // 单 session fallback:恰好 1 个文件时使用,否则拒绝猜测 + let sessionFiles: string[]; + try { + sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); + } catch { + return { status: "no_task", taskDir: null, taskTitle: null }; + } + if (sessionFiles.length === 1) { + sessionFilePath = join(sessionsDir, sessionFiles[0]); + } else { + // 0 或 ≥2 个文件且无身份标识:返回 no_task,不跨会话猜测 + return { status: "no_task", taskDir: null, taskTitle: null }; + } } - const sessionFile = sessionFiles[0]; + // --- 读取 session 数据 --- let sessionData: Record<string, unknown>; try { - sessionData = JSON.parse( - readFileSync(join(sessionsDir, sessionFile), "utf-8"), - ); + sessionData = JSON.parse(readFileSync(sessionFilePath, "utf-8")); } catch { return { status: "no_task", taskDir: null, taskTitle: null }; } @@ -276,6 +317,16 @@ export default function(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); + // Derive context key from OMP session identity and export it so + // resolveActiveTaskStatus() and spawned subprocesses (get_context.py, + // task.py) can locate the correct session runtime file. + if (!process.env.TRELLIS_CONTEXT_ID) { + const sessionId = (ctx.sessionManager as { getSessionId?: () => string }).getSessionId?.(); + if (sessionId) { + process.env.TRELLIS_CONTEXT_ID = buildContextKey("pi", "session", sessionId); + } + } + if (!projectRoot) return; if (isSubAgent) { diff --git a/packages/cli/test/commands/update.integration.test.ts b/packages/cli/test/commands/update.integration.test.ts index ef192f4cb..81a0f35ed 100644 --- a/packages/cli/test/commands/update.integration.test.ts +++ b/packages/cli/test/commands/update.integration.test.ts @@ -14,38 +14,38 @@ import inquirer from "inquirer"; // === External dependency mocks (hoisted by vitest) === vi.mock("figlet", () => ({ - default: { textSync: vi.fn(() => "TRELLIS") }, + default: { textSync: vi.fn(() => "TRELLIS") }, })); vi.mock("inquirer", () => ({ - default: { prompt: vi.fn().mockResolvedValue({ proceed: true }) }, + default: { prompt: vi.fn().mockResolvedValue({ proceed: true }) }, })); vi.mock("node:child_process", () => ({ - execSync: vi.fn().mockImplementation((cmd: string) => { - const py = process.platform === "win32" ? "python" : "python3"; - return cmd === `${py} --version` ? "Python 3.11.12" : ""; - }), + execSync: vi.fn().mockImplementation((cmd: string) => { + const py = process.platform === "win32" ? "python" : "python3"; + return cmd === `${py} --version` ? "Python 3.11.12" : ""; + }), })); const registryDownload = vi.hoisted(() => ({ - files: new Map<string, string>(), + files: new Map<string, string>(), })); vi.mock("giget", async () => { - const fs = await import("node:fs"); - const path = await import("node:path"); - return { - downloadTemplate: vi.fn( - async (_source: string, options: { dir: string }) => { - for (const [relativePath, content] of registryDownload.files) { - const targetPath = path.join(options.dir, relativePath); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, content, "utf-8"); - } - }, - ), - }; + const fs = await import("node:fs"); + const path = await import("node:path"); + return { + downloadTemplate: vi.fn( + async (_source: string, options: { dir: string }) => { + for (const [relativePath, content] of registryDownload.files) { + const targetPath = path.join(options.dir, relativePath); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, content, "utf-8"); + } + }, + ), + }; }); // === Imports === @@ -63,10 +63,10 @@ const MANAGED_FILE = `${PATHS.SCRIPTS}/get_context.py`; /** Remove a key from a hash object (avoids eslint no-dynamic-delete) */ function removeHashEntry( - obj: Record<string, unknown>, - key: string, + obj: Record<string, unknown>, + key: string, ): Record<string, unknown> { - return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key)); + return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key)); } /** @@ -74,1217 +74,1217 @@ function removeHashEntry( * Tests manipulate this map then write it back via `writeHashesV2`. */ function readHashesV2(hashFile: string): Record<string, string> { - const raw = JSON.parse(fs.readFileSync(hashFile, "utf-8")) as { - __version?: number; - hashes?: Record<string, string>; - }; - return raw.hashes ?? {}; + const raw = JSON.parse(fs.readFileSync(hashFile, "utf-8")) as { + __version?: number; + hashes?: Record<string, string>; + }; + return raw.hashes ?? {}; } /** Write a v2-shaped hashes file. */ function writeHashesV2(hashFile: string, hashes: Record<string, string>): void { - fs.writeFileSync(hashFile, JSON.stringify({ __version: 2, hashes }, null, 2)); + fs.writeFileSync(hashFile, JSON.stringify({ __version: 2, hashes }, null, 2)); } function removeSubagentsSection(content: string): string { - return content.replace( - "\n## Subagents\n\n" + - "- ALWAYS wait for all subagents to complete before yielding.\n" + - "- Spawn subagents automatically when:\n" + - " - Parallelizable work (e.g., install + verify, npm test + typecheck, multiple tasks from plan)\n" + - " - Long-running or blocking tasks where a worker can run independently.\n" + - " - Isolation for risky changes or checks\n", - "", - ); + return content.replace( + "\n## Subagents\n\n" + + "- ALWAYS wait for all subagents to complete before yielding.\n" + + "- Spawn subagents automatically when:\n" + + " - Parallelizable work (e.g., install + verify, npm test + typecheck, multiple tasks from plan)\n" + + " - Long-running or blocking tasks where a worker can run independently.\n" + + " - Isolation for risky changes or checks\n", + "", + ); } describe("update() integration", () => { - let tmpDir: string; - - /** Initialize a fresh project in tmpDir */ - async function setupProject(): Promise<void> { - await init({ yes: true, force: true }); + let tmpDir: string; + + /** Initialize a fresh project in tmpDir */ + async function setupProject(): Promise<void> { + await init({ yes: true, force: true }); + } + + function projectFile(relativePath: string): string { + return path.join(tmpDir, relativePath); + } + + function hashFilePath(): string { + return projectFile(`${DIR_NAMES.WORKFLOW}/.template-hashes.json`); + } + + function versionFilePath(): string { + return projectFile(`${DIR_NAMES.WORKFLOW}/.version`); + } + + function readProjectFile(relativePath: string): string { + return fs.readFileSync(projectFile(relativePath), "utf-8"); + } + + function writeProjectFile(relativePath: string, content: string): void { + const fullPath = projectFile(relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content, "utf-8"); + } + + /** + * Stage a project as if an older Trellis version installed pristine template + * files, then the current CLI is about to update it. The hash file records + * the older pristine content so update() must treat those files as + * auto-update candidates. + */ + function stageVersionedUpgradeProject(options: { + fromVersion: string; + pristineTemplates?: Record<string, string>; + userModifiedTemplates?: Record<string, string>; + }): void { + fs.writeFileSync(versionFilePath(), options.fromVersion); + + const hashes = readHashesV2(hashFilePath()); + for (const [relativePath, content] of Object.entries( + options.pristineTemplates ?? {}, + )) { + writeProjectFile(relativePath, content); + hashes[relativePath] = computeHash(content); } + writeHashesV2(hashFilePath(), hashes); - function projectFile(relativePath: string): string { - return path.join(tmpDir, relativePath); + for (const [relativePath, content] of Object.entries( + options.userModifiedTemplates ?? {}, + )) { + writeProjectFile(relativePath, content); } + } + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "trellis-update-int-")); + vi.spyOn(process, "cwd").mockReturnValue(tmpDir); + registryDownload.files.clear(); + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => { }; + vi.spyOn(console, "log").mockImplementation(noop); + vi.spyOn(console, "error").mockImplementation(noop); + // Mock fetch for npm registry + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ version: VERSION }), + }), + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("#1 same version update is a true no-op (zero file changes, no backup)", async () => { + await setupProject(); + + // Full snapshot before update + const snapshotBefore = new Map<string, string>(); + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else + snapshotBefore.set( + path.relative(tmpDir, full), + fs.readFileSync(full, "utf-8"), + ); + } + }; + walk(tmpDir); + + await update({}); + + // Full snapshot after update + const snapshotAfter = new Map<string, string>(); + const walk2 = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk2(full); + else + snapshotAfter.set( + path.relative(tmpDir, full), + fs.readFileSync(full, "utf-8"), + ); + } + }; + walk2(tmpDir); - function hashFilePath(): string { - return projectFile(`${DIR_NAMES.WORKFLOW}/.template-hashes.json`); + // No files added or removed + const addedFiles = [...snapshotAfter.keys()].filter( + (k) => !snapshotBefore.has(k), + ); + const removedFiles = [...snapshotBefore.keys()].filter( + (k) => !snapshotAfter.has(k), + ); + expect(addedFiles).toEqual([]); + expect(removedFiles).toEqual([]); + + // No file contents changed + const changedFiles: string[] = []; + for (const [filePath, content] of snapshotBefore) { + if (snapshotAfter.get(filePath) !== content) { + changedFiles.push(filePath); + } } + expect(changedFiles).toEqual([]); + + // No backup directory created + const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); + expect(entries.filter((e) => e.startsWith(".backup-")).length).toBe(0); + }); + + it("[issue-zcode-codex-upgrade] zcode .agents skills do not trigger legacy Codex backfill", async () => { + await init({ yes: true, force: true, zcode: true }); + + expect( + fs.existsSync(projectFile(".zcode/commands/trellis/start.md")), + ).toBe(true); + expect( + fs.existsSync(projectFile(".agents/skills/trellis-start/SKILL.md")), + ).toBe(false); + expect( + fs.existsSync(projectFile(".agents/skills/trellis-continue/SKILL.md")), + ).toBe(false); + + await update({}); + + const logOutput = vi.mocked(console.log).mock.calls.flat().join("\n"); + expect(logOutput).not.toContain("Legacy Codex detected"); + expect(fs.existsSync(projectFile(".codex"))).toBe(false); + }); + + it("#2 dry run makes no file changes even when changes exist", async () => { + await setupProject(); + + // Delete hash + file to simulate a truly new template file + const target = path.join(tmpDir, MANAGED_FILE); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = removeHashEntry( + readHashesV2(hashFile), + MANAGED_FILE, + ) as Record<string, string>; + writeHashesV2(hashFile, hashes); + fs.unlinkSync(target); - function versionFilePath(): string { - return projectFile(`${DIR_NAMES.WORKFLOW}/.version`); - } + await update({ dryRun: true }); - function readProjectFile(relativePath: string): string { - return fs.readFileSync(projectFile(relativePath), "utf-8"); - } + // File should still be missing (dry run didn't recreate it) + expect(fs.existsSync(target)).toBe(false); + // No backup directory created + const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); + expect(entries.filter((e) => e.startsWith(".backup-")).length).toBe(0); + }); - function writeProjectFile(relativePath: string, content: string): void { - const fullPath = projectFile(relativePath); - fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - fs.writeFileSync(fullPath, content, "utf-8"); - } + it("#3 user-deleted file (with stored hash) is not re-added on update", async () => { + await setupProject(); - /** - * Stage a project as if an older Trellis version installed pristine template - * files, then the current CLI is about to update it. The hash file records - * the older pristine content so update() must treat those files as - * auto-update candidates. - */ - function stageVersionedUpgradeProject(options: { - fromVersion: string; - pristineTemplates?: Record<string, string>; - userModifiedTemplates?: Record<string, string>; - }): void { - fs.writeFileSync(versionFilePath(), options.fromVersion); - - const hashes = readHashesV2(hashFilePath()); - for (const [relativePath, content] of Object.entries( - options.pristineTemplates ?? {}, - )) { - writeProjectFile(relativePath, content); - hashes[relativePath] = computeHash(content); - } - writeHashesV2(hashFilePath(), hashes); + const target = path.join(tmpDir, MANAGED_FILE); + expect(fs.existsSync(target)).toBe(true); - for (const [relativePath, content] of Object.entries( - options.userModifiedTemplates ?? {}, - )) { - writeProjectFile(relativePath, content); - } - } - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "trellis-update-int-")); - vi.spyOn(process, "cwd").mockReturnValue(tmpDir); - registryDownload.files.clear(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - const noop = () => {}; - vi.spyOn(console, "log").mockImplementation(noop); - vi.spyOn(console, "error").mockImplementation(noop); - // Mock fetch for npm registry - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ version: VERSION }), - }), - ); - }); - - afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllGlobals(); - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); + // Delete it (simulating user deletion; hash still exists in .template-hashes.json) + fs.unlinkSync(target); + expect(fs.existsSync(target)).toBe(false); - it("#1 same version update is a true no-op (zero file changes, no backup)", async () => { - await setupProject(); - - // Full snapshot before update - const snapshotBefore = new Map<string, string>(); - const walk = (dir: string) => { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) walk(full); - else - snapshotBefore.set( - path.relative(tmpDir, full), - fs.readFileSync(full, "utf-8"), - ); - } - }; - walk(tmpDir); - - await update({}); - - // Full snapshot after update - const snapshotAfter = new Map<string, string>(); - const walk2 = (dir: string) => { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) walk2(full); - else - snapshotAfter.set( - path.relative(tmpDir, full), - fs.readFileSync(full, "utf-8"), - ); - } - }; - walk2(tmpDir); - - // No files added or removed - const addedFiles = [...snapshotAfter.keys()].filter( - (k) => !snapshotBefore.has(k), - ); - const removedFiles = [...snapshotBefore.keys()].filter( - (k) => !snapshotAfter.has(k), - ); - expect(addedFiles).toEqual([]); - expect(removedFiles).toEqual([]); - - // No file contents changed - const changedFiles: string[] = []; - for (const [filePath, content] of snapshotBefore) { - if (snapshotAfter.get(filePath) !== content) { - changedFiles.push(filePath); - } - } - expect(changedFiles).toEqual([]); + await update({ force: true }); - // No backup directory created - const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); - expect(entries.filter((e) => e.startsWith(".backup-")).length).toBe(0); - }); + // File should NOT be re-created (user deleted it, hash still exists) + expect(fs.existsSync(target)).toBe(false); + }); - it("[issue-zcode-codex-upgrade] zcode .agents skills do not trigger legacy Codex backfill", async () => { - await init({ yes: true, force: true, zcode: true }); + it("#4 auto-updates file when template changed but user did not modify", async () => { + await setupProject(); - expect( - fs.existsSync(projectFile(".zcode/commands/trellis/start.md")), - ).toBe(true); - expect( - fs.existsSync(projectFile(".agents/skills/trellis-start/SKILL.md")), - ).toBe(false); - expect( - fs.existsSync(projectFile(".agents/skills/trellis-continue/SKILL.md")), - ).toBe(false); + const targetRelative = MANAGED_FILE; + const targetFull = path.join(tmpDir, targetRelative); + const templateContent = fs.readFileSync(targetFull, "utf-8"); - await update({}); + // Simulate "old template version": change file + update hash to match + const oldContent = "# Old version of script\n"; + fs.writeFileSync(targetFull, oldContent); - const logOutput = vi.mocked(console.log).mock.calls.flat().join("\n"); - expect(logOutput).not.toContain("Legacy Codex detected"); - expect(fs.existsSync(projectFile(".codex"))).toBe(false); - }); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = readHashesV2(hashFile); + hashes[targetRelative] = computeHash(oldContent); + writeHashesV2(hashFile, hashes); - it("#2 dry run makes no file changes even when changes exist", async () => { - await setupProject(); - - // Delete hash + file to simulate a truly new template file - const target = path.join(tmpDir, MANAGED_FILE); - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = removeHashEntry( - readHashesV2(hashFile), - MANAGED_FILE, - ) as Record<string, string>; - writeHashesV2(hashFile, hashes); - fs.unlinkSync(target); - - await update({ dryRun: true }); - - // File should still be missing (dry run didn't recreate it) - expect(fs.existsSync(target)).toBe(false); - // No backup directory created - const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); - expect(entries.filter((e) => e.startsWith(".backup-")).length).toBe(0); - }); + await update({ force: true }); - it("#3 user-deleted file (with stored hash) is not re-added on update", async () => { - await setupProject(); + // File should be auto-updated back to current template + expect(fs.readFileSync(targetFull, "utf-8")).toBe(templateContent); + }); - const target = path.join(tmpDir, MANAGED_FILE); - expect(fs.existsSync(target)).toBe(true); + it("#4b auto-updates legacy untracked AGENTS.md and preserves outside content", async () => { + await setupProject(); - // Delete it (simulating user deletion; hash still exists in .template-hashes.json) - fs.unlinkSync(target); - expect(fs.existsSync(target)).toBe(false); + const targetRelative = FILE_NAMES.AGENTS; + const targetFull = path.join(tmpDir, targetRelative); + const templateContent = fs.readFileSync(targetFull, "utf-8"); + const oldContent = removeSubagentsSection(templateContent); + const existingContent = `# Local instructions\n\n${oldContent}\n\n## Project Notes\n\nKeep this.`; + const expectedContent = `# Local instructions\n\n${templateContent}\n\n## Project Notes\n\nKeep this.`; - await update({ force: true }); + fs.writeFileSync(targetFull, existingContent); - // File should NOT be re-created (user deleted it, hash still exists) - expect(fs.existsSync(target)).toBe(false); - }); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = removeHashEntry( + readHashesV2(hashFile), + targetRelative, + ) as Record<string, string>; + writeHashesV2(hashFile, hashes); - it("#4 auto-updates file when template changed but user did not modify", async () => { - await setupProject(); + await update({}); - const targetRelative = MANAGED_FILE; - const targetFull = path.join(tmpDir, targetRelative); - const templateContent = fs.readFileSync(targetFull, "utf-8"); + expect(fs.readFileSync(targetFull, "utf-8")).toBe(expectedContent); + expect(readHashesV2(hashFile)[targetRelative]).toBe( + computeHash(expectedContent), + ); + }); - // Simulate "old template version": change file + update hash to match - const oldContent = "# Old version of script\n"; - fs.writeFileSync(targetFull, oldContent); + it("#4c preserves user-modified untracked AGENTS.md managed block", async () => { + await setupProject(); - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = readHashesV2(hashFile); - hashes[targetRelative] = computeHash(oldContent); - writeHashesV2(hashFile, hashes); + const targetRelative = FILE_NAMES.AGENTS; + const targetFull = path.join(tmpDir, targetRelative); + const templateContent = fs.readFileSync(targetFull, "utf-8"); + const modifiedOldContent = removeSubagentsSection(templateContent).replace( + "# Trellis Instructions", + "# Custom Trellis Instructions", + ); + fs.writeFileSync(targetFull, modifiedOldContent); - await update({ force: true }); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = removeHashEntry( + readHashesV2(hashFile), + targetRelative, + ) as Record<string, string>; + writeHashesV2(hashFile, hashes); + + await update({ skipAll: true }); + + expect(fs.readFileSync(targetFull, "utf-8")).toBe(modifiedOldContent); + }); + + it("#4d preserves user AGENTS.md without TRELLIS markers by appending the managed block", async () => { + await setupProject(); + + const targetRelative = FILE_NAMES.AGENTS; + const targetFull = path.join(tmpDir, targetRelative); + const templateContent = fs.readFileSync(targetFull, "utf-8"); + + // User has a hand-written AGENTS.md with no TRELLIS:START/END markers at + // all (predates 0.5.0-beta.18 or was authored by hand). Pre-fix behavior + // would clobber this content; post-fix should append the managed block. + const userContent = "# Project notes\n\nThings the team agreed on.\n"; + fs.writeFileSync(targetFull, userContent); + + await update({ force: true }); + + const result = fs.readFileSync(targetFull, "utf-8"); + expect(result).toContain("# Project notes"); + expect(result).toContain("Things the team agreed on."); + expect(result).toContain("<!-- TRELLIS:START -->"); + expect(result).toContain("<!-- TRELLIS:END -->"); + // Managed block should sit AFTER the user content, not replace it. + expect(result.indexOf("# Project notes")).toBeLessThan( + result.indexOf("<!-- TRELLIS:START -->"), + ); + // Tail equals the canonical template (force-applied managed block). + expect(result.endsWith(templateContent.trimEnd() + "\n")).toBe(true); + }); - // File should be auto-updated back to current template - expect(fs.readFileSync(targetFull, "utf-8")).toBe(templateContent); - }); + it("#5 force overwrites user-modified files", async () => { + await setupProject(); - it("#4b auto-updates legacy untracked AGENTS.md and preserves outside content", async () => { - await setupProject(); - - const targetRelative = FILE_NAMES.AGENTS; - const targetFull = path.join(tmpDir, targetRelative); - const templateContent = fs.readFileSync(targetFull, "utf-8"); - const oldContent = removeSubagentsSection(templateContent); - const existingContent = `# Local instructions\n\n${oldContent}\n\n## Project Notes\n\nKeep this.`; - const expectedContent = `# Local instructions\n\n${templateContent}\n\n## Project Notes\n\nKeep this.`; - - fs.writeFileSync(targetFull, existingContent); - - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = removeHashEntry( - readHashesV2(hashFile), - targetRelative, - ) as Record<string, string>; - writeHashesV2(hashFile, hashes); - - await update({}); - - expect(fs.readFileSync(targetFull, "utf-8")).toBe(expectedContent); - expect(readHashesV2(hashFile)[targetRelative]).toBe( - computeHash(expectedContent), - ); - }); + const targetFull = path.join(tmpDir, MANAGED_FILE); + const templateContent = fs.readFileSync(targetFull, "utf-8"); - it("#4c preserves user-modified untracked AGENTS.md managed block", async () => { - await setupProject(); - - const targetRelative = FILE_NAMES.AGENTS; - const targetFull = path.join(tmpDir, targetRelative); - const templateContent = fs.readFileSync(targetFull, "utf-8"); - const modifiedOldContent = removeSubagentsSection(templateContent).replace( - "# Trellis Instructions", - "# Custom Trellis Instructions", - ); - fs.writeFileSync(targetFull, modifiedOldContent); - - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = removeHashEntry( - readHashesV2(hashFile), - targetRelative, - ) as Record<string, string>; - writeHashesV2(hashFile, hashes); - - await update({ skipAll: true }); - - expect(fs.readFileSync(targetFull, "utf-8")).toBe(modifiedOldContent); - }); + // User modifies file (hash won't match) + fs.writeFileSync(targetFull, "user customized content"); - it("#4d preserves user AGENTS.md without TRELLIS markers by appending the managed block", async () => { - await setupProject(); - - const targetRelative = FILE_NAMES.AGENTS; - const targetFull = path.join(tmpDir, targetRelative); - const templateContent = fs.readFileSync(targetFull, "utf-8"); - - // User has a hand-written AGENTS.md with no TRELLIS:START/END markers at - // all (predates 0.5.0-beta.18 or was authored by hand). Pre-fix behavior - // would clobber this content; post-fix should append the managed block. - const userContent = "# Project notes\n\nThings the team agreed on.\n"; - fs.writeFileSync(targetFull, userContent); - - await update({ force: true }); - - const result = fs.readFileSync(targetFull, "utf-8"); - expect(result).toContain("# Project notes"); - expect(result).toContain("Things the team agreed on."); - expect(result).toContain("<!-- TRELLIS:START -->"); - expect(result).toContain("<!-- TRELLIS:END -->"); - // Managed block should sit AFTER the user content, not replace it. - expect(result.indexOf("# Project notes")).toBeLessThan( - result.indexOf("<!-- TRELLIS:START -->"), - ); - // Tail equals the canonical template (force-applied managed block). - expect(result.endsWith(templateContent.trimEnd() + "\n")).toBe(true); - }); + await update({ force: true }); - it("#5 force overwrites user-modified files", async () => { - await setupProject(); + expect(fs.readFileSync(targetFull, "utf-8")).toBe(templateContent); + }); - const targetFull = path.join(tmpDir, MANAGED_FILE); - const templateContent = fs.readFileSync(targetFull, "utf-8"); + it("#5b force mode does not prompt for final confirmation", async () => { + await setupProject(); - // User modifies file (hash won't match) - fs.writeFileSync(targetFull, "user customized content"); + const targetFull = path.join(tmpDir, MANAGED_FILE); + fs.writeFileSync(targetFull, "user customized content"); + vi.mocked(inquirer.prompt).mockClear(); - await update({ force: true }); + await update({ force: true }); - expect(fs.readFileSync(targetFull, "utf-8")).toBe(templateContent); - }); + expect(inquirer.prompt).not.toHaveBeenCalled(); + }); - it("#5b force mode does not prompt for final confirmation", async () => { - await setupProject(); + it("#6 skipAll preserves user-modified files", async () => { + await setupProject(); - const targetFull = path.join(tmpDir, MANAGED_FILE); - fs.writeFileSync(targetFull, "user customized content"); - vi.mocked(inquirer.prompt).mockClear(); + const targetFull = path.join(tmpDir, MANAGED_FILE); + fs.writeFileSync(targetFull, "user customized content"); - await update({ force: true }); + await update({ skipAll: true }); - expect(inquirer.prompt).not.toHaveBeenCalled(); - }); + expect(fs.readFileSync(targetFull, "utf-8")).toBe( + "user customized content", + ); + }); - it("#6 skipAll preserves user-modified files", async () => { - await setupProject(); + it("#7 createNew creates .new copy without overwriting original", async () => { + await setupProject(); - const targetFull = path.join(tmpDir, MANAGED_FILE); - fs.writeFileSync(targetFull, "user customized content"); + const targetFull = path.join(tmpDir, MANAGED_FILE); + const templateContent = fs.readFileSync(targetFull, "utf-8"); + fs.writeFileSync(targetFull, "user customized content"); - await update({ skipAll: true }); + await update({ createNew: true }); - expect(fs.readFileSync(targetFull, "utf-8")).toBe( - "user customized content", - ); - }); + // Original preserved + expect(fs.readFileSync(targetFull, "utf-8")).toBe( + "user customized content", + ); + // .new file created with template content + const newFile = targetFull + ".new"; + expect(fs.existsSync(newFile)).toBe(true); + expect(fs.readFileSync(newFile, "utf-8")).toBe(templateContent); + }); + + it("#8 updates version file after successful update", async () => { + await setupProject(); + + // Simulate older project version + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + fs.writeFileSync(versionPath, "0.0.1"); + + await update({ force: true }); + + // Version is updated even when no file changes are needed + expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); + }); + + it("#9 creates backup directory before applying changes", async () => { + await setupProject(); + + // Simulate "old template version": change file + update hash to match + // This triggers auto-update (template changed, user didn't modify) + const targetFull = path.join(tmpDir, MANAGED_FILE); + const oldContent = "# Old version of script\n"; + fs.writeFileSync(targetFull, oldContent); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = readHashesV2(hashFile); + hashes[MANAGED_FILE] = computeHash(oldContent); + writeHashesV2(hashFile, hashes); - it("#7 createNew creates .new copy without overwriting original", async () => { - await setupProject(); + await update({ force: true }); - const targetFull = path.join(tmpDir, MANAGED_FILE); - const templateContent = fs.readFileSync(targetFull, "utf-8"); - fs.writeFileSync(targetFull, "user customized content"); + const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); + const backupDirs = entries.filter((e) => e.startsWith(".backup-")); + expect(backupDirs.length).toBeGreaterThanOrEqual(1); + }); - await update({ createNew: true }); + it("#10 downgrade protection prevents update when CLI is older", async () => { + await setupProject(); - // Original preserved - expect(fs.readFileSync(targetFull, "utf-8")).toBe( - "user customized content", - ); - // .new file created with template content - const newFile = targetFull + ".new"; - expect(fs.existsSync(newFile)).toBe(true); - expect(fs.readFileSync(newFile, "utf-8")).toBe(templateContent); - }); + // Set project version to future + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + fs.writeFileSync(versionPath, "99.99.99"); - it("#8 updates version file after successful update", async () => { - await setupProject(); + await update({}); - // Simulate older project version - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - fs.writeFileSync(versionPath, "0.0.1"); + // Version should NOT be changed + expect(fs.readFileSync(versionPath, "utf-8")).toBe("99.99.99"); + }); - await update({ force: true }); + it("#11 allowDowngrade permits update when CLI is older", async () => { + await setupProject(); - // Version is updated even when no file changes are needed - expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); - }); + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + fs.writeFileSync(versionPath, "99.99.99"); - it("#9 creates backup directory before applying changes", async () => { - await setupProject(); - - // Simulate "old template version": change file + update hash to match - // This triggers auto-update (template changed, user didn't modify) - const targetFull = path.join(tmpDir, MANAGED_FILE); - const oldContent = "# Old version of script\n"; - fs.writeFileSync(targetFull, oldContent); - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = readHashesV2(hashFile); - hashes[MANAGED_FILE] = computeHash(oldContent); - writeHashesV2(hashFile, hashes); - - await update({ force: true }); - - const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); - const backupDirs = entries.filter((e) => e.startsWith(".backup-")); - expect(backupDirs.length).toBeGreaterThanOrEqual(1); + // Remove hash entry + file to simulate a truly new template file + const target = path.join(tmpDir, MANAGED_FILE); + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = removeHashEntry( + readHashesV2(hashFile), + MANAGED_FILE, + ) as Record<string, string>; + writeHashesV2(hashFile, hashes); + fs.unlinkSync(target); + + await update({ allowDowngrade: true, force: true }); + + // File recreated (truly new — no stored hash) + expect(fs.existsSync(target)).toBe(true); + // Version updated to current + expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); + }); + + it("#12 prerelease→stable upgrade with no file changes still updates .version", async () => { + await setupProject(); + + // Simulate a project at rc.6 (identical templates, just different version stamp) + const versionPath = versionFilePath(); + fs.writeFileSync(versionPath, "0.3.0-rc.6"); + + await update({}); + + // .version must be updated to the current CLI version + expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); + }); + + it("#12b versioned upgrade scenario applies auto-updates, additive config sections, and modified-file skips", async () => { + await setupProject(); + + const expectedWorkflow = replacePythonCommandLiterals(workflowMdTemplate); + const expectedGetContext = readProjectFile(MANAGED_FILE); + const userModifiedScript = `${PATHS.SCRIPTS}/add_session.py`; + const userModifiedScriptContent = "# user customized add_session.py\n"; + const oldConfigWithoutSessionAutoCommit = + "max_journal_lines: 2000\n\n" + + "# Local 0.5.10 config customization that must survive update.\n"; + const oldWorkflow = + "# Workflow\n\n" + + "## Phase Index\n\n" + + "[workflow-state:in_progress]\nlegacy body\n[/workflow-state:in_progress]\n\n" + + "#### 2.1 Implement `[required · repeatable]`\n\n" + + "[Codex]\nSpawn the implement sub-agent:\n[/Codex]\n\n" + + "[Kilo, Antigravity, Windsurf]\n" + + "1. Load the `trellis-before-dev` skill to read project guidelines\n" + + "[/Kilo, Antigravity, Windsurf]\n"; + + stageVersionedUpgradeProject({ + fromVersion: "0.5.10", + pristineTemplates: { + [PATHS.WORKFLOW_GUIDE_FILE]: oldWorkflow, + [MANAGED_FILE]: "# old get_context.py from installed template\n", + }, + userModifiedTemplates: { + [`${DIR_NAMES.WORKFLOW}/config.yaml`]: + oldConfigWithoutSessionAutoCommit, + [userModifiedScript]: userModifiedScriptContent, + }, }); - it("#10 downgrade protection prevents update when CLI is older", async () => { - await setupProject(); + await update({ skipAll: true }); - // Set project version to future - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - fs.writeFileSync(versionPath, "99.99.99"); + expect(fs.readFileSync(versionFilePath(), "utf-8")).toBe(VERSION); - await update({}); + // Hash-tracked pristine templates from the older install are whole-file + // auto-updated to the current packaged template. + expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).toBe(expectedWorkflow); + expect(readProjectFile(MANAGED_FILE)).toBe(expectedGetContext); + expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).toContain( + "[codex-inline, Kilo, Antigravity, Devin]", + ); + expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).not.toContain( + "[Codex]", + ); - // Version should NOT be changed - expect(fs.readFileSync(versionPath, "utf-8")).toBe("99.99.99"); - }); + // Version-specific additive config sections still apply to a user-modified + // config.yaml, while preserving the local content around the append. + const updatedConfig = readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`); + expect(updatedConfig).toContain( + "Local 0.5.10 config customization that must survive update.", + ); + expect(updatedConfig).toContain("Session Auto-Commit"); + expect(updatedConfig).toContain("session_auto_commit: true"); - it("#11 allowDowngrade permits update when CLI is older", async () => { - await setupProject(); - - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - fs.writeFileSync(versionPath, "99.99.99"); - - // Remove hash entry + file to simulate a truly new template file - const target = path.join(tmpDir, MANAGED_FILE); - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = removeHashEntry( - readHashesV2(hashFile), - MANAGED_FILE, - ) as Record<string, string>; - writeHashesV2(hashFile, hashes); - fs.unlinkSync(target); - - await update({ allowDowngrade: true, force: true }); - - // File recreated (truly new — no stored hash) - expect(fs.existsSync(target)).toBe(true); - // Version updated to current - expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); - }); + // User-modified template files are skipped under skipAll and their hashes + // are not rewritten to bless the local modification as a template. + expect(readProjectFile(userModifiedScript)).toBe( + userModifiedScriptContent, + ); + const hashes = readHashesV2(hashFilePath()); + expect(hashes[PATHS.WORKFLOW_GUIDE_FILE]).toBe( + computeHash(expectedWorkflow), + ); + expect(hashes[MANAGED_FILE]).toBe(computeHash(expectedGetContext)); + expect(hashes[userModifiedScript]).not.toBe( + computeHash(userModifiedScriptContent), + ); + }); - it("#12 prerelease→stable upgrade with no file changes still updates .version", async () => { - await setupProject(); + it("#13 user-edited spec/guides files are preserved after update with force", async () => { + await setupProject(); - // Simulate a project at rc.6 (identical templates, just different version stamp) - const versionPath = versionFilePath(); - fs.writeFileSync(versionPath, "0.3.0-rc.6"); + // User customizes a spec guides file + const guidesIndex = path.join(tmpDir, PATHS.SPEC, "guides", "index.md"); + expect(fs.existsSync(guidesIndex)).toBe(true); + const customContent = "# My Custom Guides\n\nEdited by user.\n"; + fs.writeFileSync(guidesIndex, customContent); - await update({}); + await update({ force: true }); - // .version must be updated to the current CLI version - expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); - }); + // User's customized content must be preserved (update should not touch spec/) + expect(fs.readFileSync(guidesIndex, "utf-8")).toBe(customContent); + }); - it("#12b versioned upgrade scenario applies auto-updates, additive config sections, and modified-file skips", async () => { - await setupProject(); - - const expectedWorkflow = replacePythonCommandLiterals(workflowMdTemplate); - const expectedGetContext = readProjectFile(MANAGED_FILE); - const userModifiedScript = `${PATHS.SCRIPTS}/add_session.py`; - const userModifiedScriptContent = "# user customized add_session.py\n"; - const oldConfigWithoutSessionAutoCommit = - "max_journal_lines: 2000\n\n" + - "# Local 0.5.10 config customization that must survive update.\n"; - const oldWorkflow = - "# Workflow\n\n" + - "## Phase Index\n\n" + - "[workflow-state:in_progress]\nlegacy body\n[/workflow-state:in_progress]\n\n" + - "#### 2.1 Implement `[required · repeatable]`\n\n" + - "[Codex]\nSpawn the implement sub-agent:\n[/Codex]\n\n" + - "[Kilo, Antigravity, Windsurf]\n" + - "1. Load the `trellis-before-dev` skill to read project guidelines\n" + - "[/Kilo, Antigravity, Windsurf]\n"; - - stageVersionedUpgradeProject({ - fromVersion: "0.5.10", - pristineTemplates: { - [PATHS.WORKFLOW_GUIDE_FILE]: oldWorkflow, - [MANAGED_FILE]: "# old get_context.py from installed template\n", - }, - userModifiedTemplates: { - [`${DIR_NAMES.WORKFLOW}/config.yaml`]: - oldConfigWithoutSessionAutoCommit, - [userModifiedScript]: userModifiedScriptContent, - }, - }); + it("#14 deleted spec directory is NOT recreated by update", async () => { + await setupProject(); - await update({ skipAll: true }); - - expect(fs.readFileSync(versionFilePath(), "utf-8")).toBe(VERSION); - - // Hash-tracked pristine templates from the older install are whole-file - // auto-updated to the current packaged template. - expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).toBe(expectedWorkflow); - expect(readProjectFile(MANAGED_FILE)).toBe(expectedGetContext); - expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).toContain( - "[codex-inline, Kilo, Antigravity, Devin]", - ); - expect(readProjectFile(PATHS.WORKFLOW_GUIDE_FILE)).not.toContain( - "[Codex]", - ); - - // Version-specific additive config sections still apply to a user-modified - // config.yaml, while preserving the local content around the append. - const updatedConfig = readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`); - expect(updatedConfig).toContain( - "Local 0.5.10 config customization that must survive update.", - ); - expect(updatedConfig).toContain("Session Auto-Commit"); - expect(updatedConfig).toContain("session_auto_commit: true"); - - // User-modified template files are skipped under skipAll and their hashes - // are not rewritten to bless the local modification as a template. - expect(readProjectFile(userModifiedScript)).toBe( - userModifiedScriptContent, - ); - const hashes = readHashesV2(hashFilePath()); - expect(hashes[PATHS.WORKFLOW_GUIDE_FILE]).toBe( - computeHash(expectedWorkflow), - ); - expect(hashes[MANAGED_FILE]).toBe(computeHash(expectedGetContext)); - expect(hashes[userModifiedScript]).not.toBe( - computeHash(userModifiedScriptContent), - ); - }); + // User deletes the entire spec directory + const specDir = path.join(tmpDir, PATHS.SPEC); + fs.rmSync(specDir, { recursive: true, force: true }); + expect(fs.existsSync(specDir)).toBe(false); - it("#13 user-edited spec/guides files are preserved after update with force", async () => { - await setupProject(); + await update({ force: true }); - // User customizes a spec guides file - const guidesIndex = path.join(tmpDir, PATHS.SPEC, "guides", "index.md"); - expect(fs.existsSync(guidesIndex)).toBe(true); - const customContent = "# My Custom Guides\n\nEdited by user.\n"; - fs.writeFileSync(guidesIndex, customContent); + // spec/ directory should NOT be recreated by update + expect(fs.existsSync(specDir)).toBe(false); + }); - await update({ force: true }); + it("#14b registry-backed pristine spec is refreshed by update", async () => { + await setupProject(); - // User's customized content must be preserved (update should not touch spec/) - expect(fs.readFileSync(guidesIndex, "utf-8")).toBe(customContent); - }); + const specFile = `${PATHS.SPEC}/index.md`; + writeProjectFile(specFile, "# remote spec v1\n"); + writeProjectFile( + `${DIR_NAMES.WORKFLOW}/config.yaml`, + `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/spec\n`, + ); + const hashes = readHashesV2(hashFilePath()); + hashes[specFile] = computeHash("# remote spec v1\n"); + writeHashesV2(hashFilePath(), hashes); + + registryDownload.files.set("index.md", "# remote spec v2\n"); + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation((input: string | URL) => { + const url = String(input); + if (url.includes("registry.npmjs.org")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ version: VERSION }), + }); + } + return Promise.resolve({ status: 404, ok: false }); + }), + ); - it("#14 deleted spec directory is NOT recreated by update", async () => { - await setupProject(); + await update({ force: true }); - // User deletes the entire spec directory - const specDir = path.join(tmpDir, PATHS.SPEC); - fs.rmSync(specDir, { recursive: true, force: true }); - expect(fs.existsSync(specDir)).toBe(false); + expect(readProjectFile(specFile)).toBe("# remote spec v2\n"); + expect(readHashesV2(hashFilePath())[specFile]).toBe( + computeHash("# remote spec v2\n"), + ); + expect(readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)).toContain( + "source: gitlab:local/registry/spec", + ); + }); - await update({ force: true }); + it("#14c registry-backed user-modified spec is preserved under skipAll", async () => { + await setupProject(); - // spec/ directory should NOT be recreated by update - expect(fs.existsSync(specDir)).toBe(false); - }); + const specFile = `${PATHS.SPEC}/index.md`; + writeProjectFile(specFile, "# local edits\n"); + writeProjectFile( + `${DIR_NAMES.WORKFLOW}/config.yaml`, + `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/spec\n`, + ); + const hashes = readHashesV2(hashFilePath()); + hashes[specFile] = computeHash("# remote spec v1\n"); + writeHashesV2(hashFilePath(), hashes); + + registryDownload.files.set("index.md", "# remote spec v2\n"); + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation((input: string | URL) => { + const url = String(input); + if (url.includes("registry.npmjs.org")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ version: VERSION }), + }); + } + return Promise.resolve({ status: 404, ok: false }); + }), + ); - it("#14b registry-backed pristine spec is refreshed by update", async () => { - await setupProject(); - - const specFile = `${PATHS.SPEC}/index.md`; - writeProjectFile(specFile, "# remote spec v1\n"); - writeProjectFile( - `${DIR_NAMES.WORKFLOW}/config.yaml`, - `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/spec\n`, - ); - const hashes = readHashesV2(hashFilePath()); - hashes[specFile] = computeHash("# remote spec v1\n"); - writeHashesV2(hashFilePath(), hashes); - - registryDownload.files.set("index.md", "# remote spec v2\n"); - vi.stubGlobal( - "fetch", - vi.fn().mockImplementation((input: string | URL) => { - const url = String(input); - if (url.includes("registry.npmjs.org")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ version: VERSION }), - }); - } - return Promise.resolve({ status: 404, ok: false }); - }), - ); - - await update({ force: true }); - - expect(readProjectFile(specFile)).toBe("# remote spec v2\n"); - expect(readHashesV2(hashFilePath())[specFile]).toBe( - computeHash("# remote spec v2\n"), - ); - expect(readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)).toContain( - "source: gitlab:local/registry/spec", - ); - }); + await update({ skipAll: true }); - it("#14c registry-backed user-modified spec is preserved under skipAll", async () => { - await setupProject(); - - const specFile = `${PATHS.SPEC}/index.md`; - writeProjectFile(specFile, "# local edits\n"); - writeProjectFile( - `${DIR_NAMES.WORKFLOW}/config.yaml`, - `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/spec\n`, - ); - const hashes = readHashesV2(hashFilePath()); - hashes[specFile] = computeHash("# remote spec v1\n"); - writeHashesV2(hashFilePath(), hashes); - - registryDownload.files.set("index.md", "# remote spec v2\n"); - vi.stubGlobal( - "fetch", - vi.fn().mockImplementation((input: string | URL) => { - const url = String(input); - if (url.includes("registry.npmjs.org")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ version: VERSION }), - }); - } - return Promise.resolve({ status: 404, ok: false }); - }), - ); - - await update({ skipAll: true }); - - expect(readProjectFile(specFile)).toBe("# local edits\n"); - expect(readHashesV2(hashFilePath())[specFile]).toBe( - computeHash("# remote spec v1\n"), - ); - }); + expect(readProjectFile(specFile)).toBe("# local edits\n"); + expect(readHashesV2(hashFilePath())[specFile]).toBe( + computeHash("# remote spec v1\n"), + ); + }); - it("#14d registry-backed marketplace template spec is refreshed by update", async () => { - await setupProject(); - - const specFile = `${PATHS.SPEC}/index.md`; - writeProjectFile(specFile, "# golang spec v1\n"); - writeProjectFile( - `${DIR_NAMES.WORKFLOW}/config.yaml`, - `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/marketplace\n template: golang-spec\n`, - ); - const hashes = readHashesV2(hashFilePath()); - hashes[specFile] = computeHash("# golang spec v1\n"); - writeHashesV2(hashFilePath(), hashes); - - registryDownload.files.set("index.md", "# golang spec v2\n"); - const index = JSON.stringify({ - version: 1, - templates: [ - { - id: "golang-spec", - type: "spec", - name: "Golang", - path: "backend", - }, - ], - }); - vi.stubGlobal( - "fetch", - vi.fn().mockImplementation((input: string | URL) => { - const url = String(input); - if (url.includes("registry.npmjs.org")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ version: VERSION }), - }); - } - return Promise.resolve({ - ok: true, - text: () => Promise.resolve(index), - }); - }), - ); - - await update({ force: true }); - - expect(readProjectFile(specFile)).toBe("# golang spec v2\n"); - expect(readHashesV2(hashFilePath())[specFile]).toBe( - computeHash("# golang spec v2\n"), - ); - }); + it("#14d registry-backed marketplace template spec is refreshed by update", async () => { + await setupProject(); - it("#15 truly new file (no stored hash) is still added", async () => { - await setupProject(); - - // The hash file should exist - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = removeHashEntry( - readHashesV2(hashFile), - MANAGED_FILE, - ) as Record<string, string>; - - // Remove a hash entry AND the file (simulates a truly new template) - const targetPath = path.join(tmpDir, MANAGED_FILE); - writeHashesV2(hashFile, hashes); - fs.unlinkSync(targetPath); - - // Run update - await update({ force: true }); - - // File SHOULD be created (no hash = truly new) - expect(fs.existsSync(targetPath)).toBe(true); + const specFile = `${PATHS.SPEC}/index.md`; + writeProjectFile(specFile, "# golang spec v1\n"); + writeProjectFile( + `${DIR_NAMES.WORKFLOW}/config.yaml`, + `${readProjectFile(`${DIR_NAMES.WORKFLOW}/config.yaml`)}\nregistry:\n spec:\n source: gitlab:local/registry/marketplace\n template: golang-spec\n`, + ); + const hashes = readHashesV2(hashFilePath()); + hashes[specFile] = computeHash("# golang spec v1\n"); + writeHashesV2(hashFilePath(), hashes); + + registryDownload.files.set("index.md", "# golang spec v2\n"); + const index = JSON.stringify({ + version: 1, + templates: [ + { + id: "golang-spec", + type: "spec", + name: "Golang", + path: "backend", + }, + ], }); + vi.stubGlobal( + "fetch", + vi.fn().mockImplementation((input: string | URL) => { + const url = String(input); + if (url.includes("registry.npmjs.org")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ version: VERSION }), + }); + } + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(index), + }); + }), + ); - it("#16 config.yaml update.skip prevents file from being updated", async () => { - await setupProject(); + await update({ force: true }); - // Pick a managed template file - const targetPath = path.join(tmpDir, MANAGED_FILE); + expect(readProjectFile(specFile)).toBe("# golang spec v2\n"); + expect(readHashesV2(hashFilePath())[specFile]).toBe( + computeHash("# golang spec v2\n"), + ); + }); - // Add skip config - const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); - const configContent = fs.readFileSync(configPath, "utf-8"); - fs.writeFileSync( - configPath, - configContent + `\nupdate:\n skip:\n - ${MANAGED_FILE}\n`, - ); + it("#15 truly new file (no stored hash) is still added", async () => { + await setupProject(); - // Modify the file so it would normally trigger a change - fs.writeFileSync(targetPath, "# modified by user\n"); + // The hash file should exist + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = removeHashEntry( + readHashesV2(hashFile), + MANAGED_FILE, + ) as Record<string, string>; + + // Remove a hash entry AND the file (simulates a truly new template) + const targetPath = path.join(tmpDir, MANAGED_FILE); + writeHashesV2(hashFile, hashes); + fs.unlinkSync(targetPath); + + // Run update + await update({ force: true }); + + // File SHOULD be created (no hash = truly new) + expect(fs.existsSync(targetPath)).toBe(true); + }); + + it("#16 config.yaml update.skip prevents file from being updated", async () => { + await setupProject(); + + // Pick a managed template file + const targetPath = path.join(tmpDir, MANAGED_FILE); + + // Add skip config + const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); + const configContent = fs.readFileSync(configPath, "utf-8"); + fs.writeFileSync( + configPath, + configContent + `\nupdate:\n skip:\n - ${MANAGED_FILE}\n`, + ); - // Run update - await update({ force: true }); + // Modify the file so it would normally trigger a change + fs.writeFileSync(targetPath, "# modified by user\n"); - // File should NOT be overwritten (it's in skip list) - expect(fs.readFileSync(targetPath, "utf-8")).toBe("# modified by user\n"); - }); + // Run update + await update({ force: true }); - it("#17 config.yaml update.skip with directory path skips all files under it", async () => { - await setupProject(); - - // Add skip config for the scripts/common/ directory - const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); - const configContent = fs.readFileSync(configPath, "utf-8"); - const skipDir = `${PATHS.SCRIPTS}/common/`; - fs.writeFileSync( - configPath, - configContent + `\nupdate:\n skip:\n - ${skipDir}\n`, - ); - - // Modify a file under the skipped directory - const targetPath = path.join(tmpDir, PATHS.SCRIPTS, "common", "paths.py"); - expect(fs.existsSync(targetPath)).toBe(true); - fs.writeFileSync(targetPath, "# user modified paths.py\n"); - - // Run update - await update({ force: true }); - - // File should NOT be overwritten (its directory is in skip list) - expect(fs.readFileSync(targetPath, "utf-8")).toBe( - "# user modified paths.py\n", - ); - }); + // File should NOT be overwritten (it's in skip list) + expect(fs.readFileSync(targetPath, "utf-8")).toBe("# modified by user\n"); + }); - it("#18 safe-file-delete preserves user-modified deprecated file", async () => { - await setupProject(); + it("#17 config.yaml update.skip with directory path skips all files under it", async () => { + await setupProject(); - // Create a deprecated file that exists in the 0.4.0-beta.1 safe-file-delete manifest - // but with user-modified content (hash won't match allowed_hashes) - const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); - fs.mkdirSync(deprecatedDir, { recursive: true }); - const deprecatedFile = path.join(deprecatedDir, "before-backend-dev.md"); - const userContent = - "# My customized before-backend-dev command\nUser edited this.\n"; - fs.writeFileSync(deprecatedFile, userContent); + // Add skip config for the scripts/common/ directory + const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); + const configContent = fs.readFileSync(configPath, "utf-8"); + const skipDir = `${PATHS.SCRIPTS}/common/`; + fs.writeFileSync( + configPath, + configContent + `\nupdate:\n skip:\n - ${skipDir}\n`, + ); - await update({ force: true }); + // Modify a file under the skipped directory + const targetPath = path.join(tmpDir, PATHS.SCRIPTS, "common", "paths.py"); + expect(fs.existsSync(targetPath)).toBe(true); + fs.writeFileSync(targetPath, "# user modified paths.py\n"); - // File should be preserved (hash doesn't match allowed_hashes) - expect(fs.existsSync(deprecatedFile)).toBe(true); - expect(fs.readFileSync(deprecatedFile, "utf-8")).toBe(userContent); - }); + // Run update + await update({ force: true }); - it("#19 safe-file-delete handles missing deprecated files without crash", async () => { - await setupProject(); + // File should NOT be overwritten (its directory is in skip list) + expect(fs.readFileSync(targetPath, "utf-8")).toBe( + "# user modified paths.py\n", + ); + }); + + it("#18 safe-file-delete preserves user-modified deprecated file", async () => { + await setupProject(); + + // Create a deprecated file that exists in the 0.4.0-beta.1 safe-file-delete manifest + // but with user-modified content (hash won't match allowed_hashes) + const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); + fs.mkdirSync(deprecatedDir, { recursive: true }); + const deprecatedFile = path.join(deprecatedDir, "before-backend-dev.md"); + const userContent = + "# My customized before-backend-dev command\nUser edited this.\n"; + fs.writeFileSync(deprecatedFile, userContent); + + await update({ force: true }); + + // File should be preserved (hash doesn't match allowed_hashes) + expect(fs.existsSync(deprecatedFile)).toBe(true); + expect(fs.readFileSync(deprecatedFile, "utf-8")).toBe(userContent); + }); + + it("#19 safe-file-delete handles missing deprecated files without crash", async () => { + await setupProject(); + + // Simulate upgrading from an old version — deprecated files don't exist + // The manifest has safe-file-delete entries for .claude/commands/trellis/before-backend-dev.md etc. + // but init() doesn't create them (templates removed). update() should not crash. + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + fs.writeFileSync(versionPath, "0.3.7"); + + // This should complete without errors even though deprecated files don't exist + await update({ force: true }); + + // Version updated successfully + expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); + }); + + // Original template content for check-backend.md (deleted in 0.4.0-beta.1). + // Hash: 4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3 + const ORIGINAL_CHECK_BACKEND_CONTENT = [ + "Check if the code you just wrote follows the backend development guidelines.", + "", + "Execute these steps:", + "1. Run `git status` to see modified files", + "2. Read `.trellis/spec/backend/index.md` to understand which guidelines apply", + "3. Based on what you changed, read the relevant guideline files:", + " - Database changes → `.trellis/spec/backend/database-guidelines.md`", + " - Error handling → `.trellis/spec/backend/error-handling.md`", + " - Logging changes → `.trellis/spec/backend/logging-guidelines.md`", + " - Type changes → `.trellis/spec/backend/type-safety.md`", + " - Any changes → `.trellis/spec/backend/quality-guidelines.md`", + "4. Review your code against the guidelines", + "5. Report any violations and fix them if found", + "", + ].join("\n"); + + it("#20 safe-file-delete respects update.skip for deprecated files", async () => { + await setupProject(); + + // Sanity: content hash must match the manifest's allowed_hashes + expect(computeHash(ORIGINAL_CHECK_BACKEND_CONTENT)).toBe( + "4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3", + ); - // Simulate upgrading from an old version — deprecated files don't exist - // The manifest has safe-file-delete entries for .claude/commands/trellis/before-backend-dev.md etc. - // but init() doesn't create them (templates removed). update() should not crash. - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - fs.writeFileSync(versionPath, "0.3.7"); + // Create a deprecated file with original content (hash matches allowed_hashes) + // Without update.skip, collectSafeFileDeletes() would delete this file. + const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); + fs.mkdirSync(deprecatedDir, { recursive: true }); + const deprecatedFile = path.join(deprecatedDir, "check-backend.md"); + fs.writeFileSync(deprecatedFile, ORIGINAL_CHECK_BACKEND_CONTENT); + + // Add the deprecated file's directory to update.skip + const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); + const configContent = fs.readFileSync(configPath, "utf-8"); + fs.writeFileSync( + configPath, + configContent + `\nupdate:\n skip:\n - .claude/commands/trellis/\n`, + ); - // This should complete without errors even though deprecated files don't exist - await update({ force: true }); + await update({ force: true }); - // Version updated successfully - expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); - }); + // File should be preserved (directory is in update.skip, overriding safe-file-delete) + expect(fs.existsSync(deprecatedFile)).toBe(true); + expect(fs.readFileSync(deprecatedFile, "utf-8")).toBe( + ORIGINAL_CHECK_BACKEND_CONTENT, + ); + }); - // Original template content for check-backend.md (deleted in 0.4.0-beta.1). - // Hash: 4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3 - const ORIGINAL_CHECK_BACKEND_CONTENT = [ - "Check if the code you just wrote follows the backend development guidelines.", - "", - "Execute these steps:", - "1. Run `git status` to see modified files", - "2. Read `.trellis/spec/backend/index.md` to understand which guidelines apply", - "3. Based on what you changed, read the relevant guideline files:", - " - Database changes → `.trellis/spec/backend/database-guidelines.md`", - " - Error handling → `.trellis/spec/backend/error-handling.md`", - " - Logging changes → `.trellis/spec/backend/logging-guidelines.md`", - " - Type changes → `.trellis/spec/backend/type-safety.md`", - " - Any changes → `.trellis/spec/backend/quality-guidelines.md`", - "4. Review your code against the guidelines", - "5. Report any violations and fix them if found", - "", - ].join("\n"); - - it("#20 safe-file-delete respects update.skip for deprecated files", async () => { - await setupProject(); - - // Sanity: content hash must match the manifest's allowed_hashes - expect(computeHash(ORIGINAL_CHECK_BACKEND_CONTENT)).toBe( - "4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3", - ); - - // Create a deprecated file with original content (hash matches allowed_hashes) - // Without update.skip, collectSafeFileDeletes() would delete this file. - const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); - fs.mkdirSync(deprecatedDir, { recursive: true }); - const deprecatedFile = path.join(deprecatedDir, "check-backend.md"); - fs.writeFileSync(deprecatedFile, ORIGINAL_CHECK_BACKEND_CONTENT); - - // Add the deprecated file's directory to update.skip - const configPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, "config.yaml"); - const configContent = fs.readFileSync(configPath, "utf-8"); - fs.writeFileSync( - configPath, - configContent + `\nupdate:\n skip:\n - .claude/commands/trellis/\n`, - ); - - await update({ force: true }); - - // File should be preserved (directory is in update.skip, overriding safe-file-delete) - expect(fs.existsSync(deprecatedFile)).toBe(true); - expect(fs.readFileSync(deprecatedFile, "utf-8")).toBe( - ORIGINAL_CHECK_BACKEND_CONTENT, - ); - }); + it("#21 safe-file-delete deletes file when hash matches allowed_hashes", async () => { + await setupProject(); - it("#21 safe-file-delete deletes file when hash matches allowed_hashes", async () => { - await setupProject(); + // Sanity: content hash must match the manifest's allowed_hashes + expect(computeHash(ORIGINAL_CHECK_BACKEND_CONTENT)).toBe( + "4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3", + ); - // Sanity: content hash must match the manifest's allowed_hashes - expect(computeHash(ORIGINAL_CHECK_BACKEND_CONTENT)).toBe( - "4e81a28d681ea770f780df55a212fd504ce21ee49b44ba16023b74b5c243cef3", - ); + // Create deprecated file with original content (hash matches allowed_hashes) + const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); + fs.mkdirSync(deprecatedDir, { recursive: true }); + const deprecatedFile = path.join(deprecatedDir, "check-backend.md"); + fs.writeFileSync(deprecatedFile, ORIGINAL_CHECK_BACKEND_CONTENT); - // Create deprecated file with original content (hash matches allowed_hashes) - const deprecatedDir = path.join(tmpDir, ".claude", "commands", "trellis"); - fs.mkdirSync(deprecatedDir, { recursive: true }); - const deprecatedFile = path.join(deprecatedDir, "check-backend.md"); - fs.writeFileSync(deprecatedFile, ORIGINAL_CHECK_BACKEND_CONTENT); + await update({ force: true }); - await update({ force: true }); + // File should be DELETED (hash matched allowed_hashes, no update.skip protection) + expect(fs.existsSync(deprecatedFile)).toBe(false); + }); - // File should be DELETED (hash matched allowed_hashes, no update.skip protection) - expect(fs.existsSync(deprecatedFile)).toBe(false); - }); + it("#22 preserves existing Claude statusLine config and hook file on update", async () => { + await init({ yes: true, force: true, claude: true }); - it("#22 preserves existing Claude statusLine config and hook file on update", async () => { - await init({ yes: true, force: true, claude: true }); - - const settingsPath = path.join(tmpDir, ".claude", "settings.json"); - const statusLinePath = path.join( - tmpDir, - ".claude", - "hooks", - "statusline.py", - ); - const expectedPythonCmd = - process.platform === "win32" ? "python" : "python3"; - const statusLineConfig = { - type: "command", - command: `${expectedPythonCmd} .claude/hooks/statusline.py`, - }; - - const settings = JSON.parse( - fs.readFileSync(settingsPath, "utf-8"), - ) as Record<string, unknown>; - settings.statusLine = statusLineConfig; - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n"); - fs.writeFileSync(statusLinePath, "# existing local statusline\n"); - - await update({ force: true }); - - expect(fs.existsSync(statusLinePath)).toBe(true); - const updatedSettings = JSON.parse( - fs.readFileSync(settingsPath, "utf-8"), - ) as Record<string, unknown>; - expect(updatedSettings.statusLine).toEqual(statusLineConfig); - expect(updatedSettings.hooks).toBeDefined(); - }); + const settingsPath = path.join(tmpDir, ".claude", "settings.json"); + const statusLinePath = path.join( + tmpDir, + ".claude", + "hooks", + "statusline.py", + ); + const expectedPythonCmd = + process.platform === "win32" ? "python" : "python3"; + const statusLineConfig = { + type: "command", + command: `${expectedPythonCmd} .claude/hooks/statusline.py`, + }; - it("#22a does not install statusline on update for opted-out projects", async () => { - await init({ yes: true, force: true, claude: true }); - - const statusLinePath = path.join( - tmpDir, - ".claude", - "hooks", - "statusline.py", - ); - expect(fs.existsSync(statusLinePath)).toBe(false); - - await update({ force: true }); - - // statusline.py must NOT enter the template walk as a `newFiles` install - expect(fs.existsSync(statusLinePath)).toBe(false); - const settings = JSON.parse( - fs.readFileSync(path.join(tmpDir, ".claude", "settings.json"), "utf-8"), - ) as Record<string, unknown>; - expect(settings).not.toHaveProperty("statusLine"); - }); + const settings = JSON.parse( + fs.readFileSync(settingsPath, "utf-8"), + ) as Record<string, unknown>; + settings.statusLine = statusLineConfig; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n"); + fs.writeFileSync(statusLinePath, "# existing local statusline\n"); + + await update({ force: true }); + + expect(fs.existsSync(statusLinePath)).toBe(true); + const updatedSettings = JSON.parse( + fs.readFileSync(settingsPath, "utf-8"), + ) as Record<string, unknown>; + expect(updatedSettings.statusLine).toEqual(statusLineConfig); + expect(updatedSettings.hooks).toBeDefined(); + }); + + it("#22a does not install statusline on update for opted-out projects", async () => { + await init({ yes: true, force: true, claude: true }); + + const statusLinePath = path.join( + tmpDir, + ".claude", + "hooks", + "statusline.py", + ); + expect(fs.existsSync(statusLinePath)).toBe(false); + + await update({ force: true }); + + // statusline.py must NOT enter the template walk as a `newFiles` install + expect(fs.existsSync(statusLinePath)).toBe(false); + const settings = JSON.parse( + fs.readFileSync(path.join(tmpDir, ".claude", "settings.json"), "utf-8"), + ) as Record<string, unknown>; + expect(settings).not.toHaveProperty("statusLine"); + }); + + it("#22b preserves a --with-statusline install across update", async () => { + await init({ yes: true, force: true, claude: true, withStatusline: true }); + + const settingsPath = path.join(tmpDir, ".claude", "settings.json"); + const statusLinePath = path.join( + tmpDir, + ".claude", + "hooks", + "statusline.py", + ); - it("#22b preserves a --with-statusline install across update", async () => { - await init({ yes: true, force: true, claude: true, withStatusline: true }); - - const settingsPath = path.join(tmpDir, ".claude", "settings.json"); - const statusLinePath = path.join( - tmpDir, - ".claude", - "hooks", - "statusline.py", - ); - - expect(fs.existsSync(statusLinePath)).toBe(true); - const hookContentBefore = fs.readFileSync(statusLinePath, "utf-8"); - const settingsBefore = fs.readFileSync(settingsPath, "utf-8"); - expect( - (JSON.parse(settingsBefore) as Record<string, unknown>).statusLine, - ).toBeDefined(); - - await update({ force: true }); - - expect(fs.existsSync(statusLinePath)).toBe(true); - expect(fs.readFileSync(statusLinePath, "utf-8")).toBe(hookContentBefore); - // Byte-identical, not just deep-equal: init's injectStatusLine must - // produce exactly what preserveExistingClaudeStatusLine re-derives - // (statusLine appended last). Any drift — even key order — makes update - // flag a phantom settings.json change on every fresh opted-in project. - expect(fs.readFileSync(settingsPath, "utf-8")).toBe(settingsBefore); + expect(fs.existsSync(statusLinePath)).toBe(true); + const hookContentBefore = fs.readFileSync(statusLinePath, "utf-8"); + const settingsBefore = fs.readFileSync(settingsPath, "utf-8"); + expect( + (JSON.parse(settingsBefore) as Record<string, unknown>).statusLine, + ).toBeDefined(); + + await update({ force: true }); + + expect(fs.existsSync(statusLinePath)).toBe(true); + expect(fs.readFileSync(statusLinePath, "utf-8")).toBe(hookContentBefore); + // Byte-identical, not just deep-equal: init's injectStatusLine must + // produce exactly what preserveExistingClaudeStatusLine re-derives + // (statusLine appended last). Any drift — even key order — makes update + // flag a phantom settings.json change on every fresh opted-in project. + expect(fs.readFileSync(settingsPath, "utf-8")).toBe(settingsBefore); + }); + + // --- Breaking-change migration gate (v0.5.0-beta.0+) --- + // Gate: if upgrading from a version that spans a breaking manifest with + // recommendMigrate=true, `update` must be invoked with --migrate (or --dry-run + // for preview). Without either, exit 1 with a clear error. + + /** Simulate a 0.4.0 project by writing a legacy command file that the manifest renames */ + function stageLegacy040Project(): void { + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + fs.writeFileSync(versionPath, "0.4.0"); + // Create one legacy file that matches a `rename` entry in 0.5.0-beta.0 manifest. + // Without this, classifyMigrations finds no work → early-exit before gate. + const legacyDir = path.join(tmpDir, ".claude", "commands", "trellis"); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, "before-dev.md"), "legacy content"); + } + + /** Delete the post-init target so classifyMigrations hits the "new doesn't exist" + * branch and respects `isTemplateModified` on the source (→ confirm bucket). */ + function clearMigrationTarget(): void { + fs.rmSync(path.join(tmpDir, ".claude/skills/trellis-before-dev"), { + recursive: true, + force: true, }); + } + + it("#22 breaking-change gate exits 1 when --migrate is missing", async () => { + await setupProject(); + stageLegacy040Project(); + + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + await update({}); + + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("#23 breaking-change gate allows --dry-run without --migrate", async () => { + await setupProject(); + stageLegacy040Project(); + + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + await update({ dryRun: true }); + + // Gate must not fire for preview mode (users need to inspect before migrating) + expect(exitSpy).not.toHaveBeenCalled(); + }); + + it("#24 breaking-change gate allows --migrate to proceed", async () => { + await setupProject(); + stageLegacy040Project(); + + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + await update({ migrate: true, force: true }); + + // Gate passes when --migrate is present; update proceeds to completion + expect(exitSpy).not.toHaveBeenCalled(); + // Version must advance to current CLI after the migrate run + const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); + expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); + }); + + // The [b] Backup-rename path in the confirm prompt promises "keeps a .backup + // copy". Previously it was identical to [r] (both relied on the full project + // snapshot). We now write an INLINE .backup next to the new path so users can + // diff/merge their customizations without digging through .trellis/.backup-*/. + /** Install a mock that returns a specific migration choice for the per-file prompt + * and {proceed: true} for the top-level confirm. Resolves the flakiness of + * matching on `name` field in the dynamic import path. */ + async function installChoiceMock( + choice: "rename" | "backup-rename" | "skip", + ) { + const inquirer = (await import("inquirer")).default; + vi.mocked(inquirer.prompt).mockImplementation(((questions: unknown) => { + const q = Array.isArray(questions) ? questions[0] : questions; + const name = (q as { name?: string }).name; + if (name === "choice") return Promise.resolve({ choice }); + return Promise.resolve({ proceed: true }); + }) as never); + } + + // The [b] Backup-rename path in the confirm prompt promises "keeps a .backup + // copy". Previously it was identical to [r] (both relied on the full project + // snapshot). We now write an INLINE .backup next to the new path so users can + // diff/merge their customizations without digging through .trellis/.backup-*/. + it("#25 backup-rename leaves inline <new-path>.backup with original content", async () => { + await setupProject(); + stageLegacy040Project(); + clearMigrationTarget(); + + // User-modified content that differs from the 0.5 template (forces confirm) + const legacyPath = path.join( + tmpDir, + ".claude/commands/trellis/before-dev.md", + ); + const userContent = "## My custom before-dev notes\nEdited by user.\n"; + fs.writeFileSync(legacyPath, userContent); - // --- Breaking-change migration gate (v0.5.0-beta.0+) --- - // Gate: if upgrading from a version that spans a breaking manifest with - // recommendMigrate=true, `update` must be invoked with --migrate (or --dry-run - // for preview). Without either, exit 1 with a clear error. - - /** Simulate a 0.4.0 project by writing a legacy command file that the manifest renames */ - function stageLegacy040Project(): void { - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - fs.writeFileSync(versionPath, "0.4.0"); - // Create one legacy file that matches a `rename` entry in 0.5.0-beta.0 manifest. - // Without this, classifyMigrations finds no work → early-exit before gate. - const legacyDir = path.join(tmpDir, ".claude", "commands", "trellis"); - fs.mkdirSync(legacyDir, { recursive: true }); - fs.writeFileSync(path.join(legacyDir, "before-dev.md"), "legacy content"); - } - - /** Delete the post-init target so classifyMigrations hits the "new doesn't exist" - * branch and respects `isTemplateModified` on the source (→ confirm bucket). */ - function clearMigrationTarget(): void { - fs.rmSync(path.join(tmpDir, ".claude/skills/trellis-before-dev"), { - recursive: true, - force: true, - }); - } - - it("#22 breaking-change gate exits 1 when --migrate is missing", async () => { - await setupProject(); - stageLegacy040Project(); - - const exitSpy = vi - .spyOn(process, "exit") - .mockImplementation(() => undefined as never); - - await update({}); + await installChoiceMock("backup-rename"); - expect(exitSpy).toHaveBeenCalledWith(1); - }); + await update({ migrate: true }); - it("#23 breaking-change gate allows --dry-run without --migrate", async () => { - await setupProject(); - stageLegacy040Project(); + // After migration: + // - new-path exists (rename completed) + // - new-path.backup exists with the user's content (inline preservation) + // - old-path is gone + const newPath = path.join( + tmpDir, + ".claude/skills/trellis-before-dev/SKILL.md", + ); + expect(fs.existsSync(newPath)).toBe(true); + expect(fs.existsSync(newPath + ".backup")).toBe(true); + expect(fs.readFileSync(newPath + ".backup", "utf-8")).toBe(userContent); + expect(fs.existsSync(legacyPath)).toBe(false); + }); + + it("#26 rename-anyway does NOT leave an inline .backup (relies on project snapshot)", async () => { + await setupProject(); + stageLegacy040Project(); + clearMigrationTarget(); + + const legacyPath = path.join( + tmpDir, + ".claude/commands/trellis/before-dev.md", + ); + fs.writeFileSync(legacyPath, "## user edits\n"); - const exitSpy = vi - .spyOn(process, "exit") - .mockImplementation(() => undefined as never); + await installChoiceMock("rename"); - await update({ dryRun: true }); + await update({ migrate: true }); - // Gate must not fire for preview mode (users need to inspect before migrating) - expect(exitSpy).not.toHaveBeenCalled(); + const newPath = path.join( + tmpDir, + ".claude/skills/trellis-before-dev/SKILL.md", + ); + expect(fs.existsSync(newPath)).toBe(true); + // No inline .backup — the full-project snapshot under .trellis/.backup-* + // is the single source of recovery for this mode. + expect(fs.existsSync(newPath + ".backup")).toBe(false); + }); + + it("#27 backup skips managed node_modules dependency trees", async () => { + await setupProject(); + + const opencodeRoot = path.join(tmpDir, ".opencode"); + fs.mkdirSync(path.join(opencodeRoot, "node_modules", "zod"), { + recursive: true, }); + fs.writeFileSync(path.join(opencodeRoot, "package.json"), "{}\n"); + fs.writeFileSync( + path.join(opencodeRoot, "node_modules", "zod", "index.js"), + "module.exports = {};\n", + ); - it("#24 breaking-change gate allows --migrate to proceed", async () => { - await setupProject(); - stageLegacy040Project(); - - const exitSpy = vi - .spyOn(process, "exit") - .mockImplementation(() => undefined as never); - - await update({ migrate: true, force: true }); - - // Gate passes when --migrate is present; update proceeds to completion - expect(exitSpy).not.toHaveBeenCalled(); - // Version must advance to current CLI after the migrate run - const versionPath = path.join(tmpDir, DIR_NAMES.WORKFLOW, ".version"); - expect(fs.readFileSync(versionPath, "utf-8")).toBe(VERSION); - }); + // Trigger an update that creates a backup. + const targetFull = path.join(tmpDir, MANAGED_FILE); + fs.writeFileSync(targetFull, "user customized content"); - // The [b] Backup-rename path in the confirm prompt promises "keeps a .backup - // copy". Previously it was identical to [r] (both relied on the full project - // snapshot). We now write an INLINE .backup next to the new path so users can - // diff/merge their customizations without digging through .trellis/.backup-*/. - /** Install a mock that returns a specific migration choice for the per-file prompt - * and {proceed: true} for the top-level confirm. Resolves the flakiness of - * matching on `name` field in the dynamic import path. */ - async function installChoiceMock( - choice: "rename" | "backup-rename" | "skip", - ) { - const inquirer = (await import("inquirer")).default; - vi.mocked(inquirer.prompt).mockImplementation(((questions: unknown) => { - const q = Array.isArray(questions) ? questions[0] : questions; - const name = (q as { name?: string }).name; - if (name === "choice") return Promise.resolve({ choice }); - return Promise.resolve({ proceed: true }); - }) as never); - } + await update({ force: true }); - // The [b] Backup-rename path in the confirm prompt promises "keeps a .backup - // copy". Previously it was identical to [r] (both relied on the full project - // snapshot). We now write an INLINE .backup next to the new path so users can - // diff/merge their customizations without digging through .trellis/.backup-*/. - it("#25 backup-rename leaves inline <new-path>.backup with original content", async () => { - await setupProject(); - stageLegacy040Project(); - clearMigrationTarget(); - - // User-modified content that differs from the 0.5 template (forces confirm) - const legacyPath = path.join( - tmpDir, - ".claude/commands/trellis/before-dev.md", - ); - const userContent = "## My custom before-dev notes\nEdited by user.\n"; - fs.writeFileSync(legacyPath, userContent); - - await installChoiceMock("backup-rename"); - - await update({ migrate: true }); - - // After migration: - // - new-path exists (rename completed) - // - new-path.backup exists with the user's content (inline preservation) - // - old-path is gone - const newPath = path.join( - tmpDir, - ".claude/skills/trellis-before-dev/SKILL.md", - ); - expect(fs.existsSync(newPath)).toBe(true); - expect(fs.existsSync(newPath + ".backup")).toBe(true); - expect(fs.readFileSync(newPath + ".backup", "utf-8")).toBe(userContent); - expect(fs.existsSync(legacyPath)).toBe(false); - }); + const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); + const backupDirs = entries.filter((e) => e.startsWith(".backup-")); + expect(backupDirs.length).toBe(1); - it("#26 rename-anyway does NOT leave an inline .backup (relies on project snapshot)", async () => { - await setupProject(); - stageLegacy040Project(); - clearMigrationTarget(); - - const legacyPath = path.join( - tmpDir, - ".claude/commands/trellis/before-dev.md", - ); - fs.writeFileSync(legacyPath, "## user edits\n"); - - await installChoiceMock("rename"); - - await update({ migrate: true }); - - const newPath = path.join( - tmpDir, - ".claude/skills/trellis-before-dev/SKILL.md", - ); - expect(fs.existsSync(newPath)).toBe(true); - // No inline .backup — the full-project snapshot under .trellis/.backup-* - // is the single source of recovery for this mode. - expect(fs.existsSync(newPath + ".backup")).toBe(false); - }); + const backupDir = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + backupDirs[0] as string, + ); + expect( + fs.existsSync(path.join(backupDir, ".opencode", "package.json")), + ).toBe(true); + expect( + fs.existsSync(path.join(backupDir, ".opencode", "node_modules")), + ).toBe(false); + }); + + it("#workflow-md-r4 updates workflow.md as one runtime template when hash-tracked", async () => { + await setupProject(); + + const workflowPath = path.join(tmpDir, PATHS.WORKFLOW_GUIDE_FILE); + const staleWorkflow = + "# Workflow\n\n" + + "## Phase Index\n\n" + + "[workflow-state:in_progress]\nlegacy body\n[/workflow-state:in_progress]\n\n" + + "#### 2.1 Implement `[required · repeatable]`\n\n" + + "[Codex]\nSpawn the implement sub-agent:\n[/Codex]\n\n" + + "[Kilo, Antigravity, Windsurf]\n" + + "1. Load the `trellis-before-dev` skill to read project guidelines\n" + + "[/Kilo, Antigravity, Windsurf]\n"; + + fs.writeFileSync(workflowPath, staleWorkflow, "utf-8"); + + // Simulate an older installed workflow.md that is still pristine relative + // to the version that installed it. Update must replace the whole file: + // platform markers outside [workflow-state:*] blocks are runtime-parsed too. + const hashFile = path.join( + tmpDir, + DIR_NAMES.WORKFLOW, + ".template-hashes.json", + ); + const hashes = readHashesV2(hashFile); + hashes[PATHS.WORKFLOW_GUIDE_FILE] = computeHash(staleWorkflow); + writeHashesV2(hashFile, hashes); - it("#27 backup skips managed node_modules dependency trees", async () => { - await setupProject(); + await update({ force: true }); - const opencodeRoot = path.join(tmpDir, ".opencode"); - fs.mkdirSync(path.join(opencodeRoot, "node_modules", "zod"), { - recursive: true, - }); - fs.writeFileSync(path.join(opencodeRoot, "package.json"), "{}\n"); - fs.writeFileSync( - path.join(opencodeRoot, "node_modules", "zod", "index.js"), - "module.exports = {};\n", - ); - - // Trigger an update that creates a backup. - const targetFull = path.join(tmpDir, MANAGED_FILE); - fs.writeFileSync(targetFull, "user customized content"); - - await update({ force: true }); - - const entries = fs.readdirSync(path.join(tmpDir, DIR_NAMES.WORKFLOW)); - const backupDirs = entries.filter((e) => e.startsWith(".backup-")); - expect(backupDirs.length).toBe(1); - - const backupDir = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - backupDirs[0] as string, - ); - expect( - fs.existsSync(path.join(backupDir, ".opencode", "package.json")), - ).toBe(true); - expect( - fs.existsSync(path.join(backupDir, ".opencode", "node_modules")), - ).toBe(false); - }); + const updated = fs.readFileSync(workflowPath, "utf-8"); + expect(updated).toBe(replacePythonCommandLiterals(workflowMdTemplate)); + expect(updated).toContain( + "[codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae]", + ); + expect(updated).toContain( + "[/Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi, Oh My Pi]", + ); + expect(updated).toContain("[codex-inline, Kilo, Antigravity, Devin]"); + expect(updated).not.toContain("[Codex]"); + expect(updated).not.toContain("[Kilo, Antigravity, Windsurf]"); + expect(updated).not.toContain("legacy body"); - it("#workflow-md-r4 updates workflow.md as one runtime template when hash-tracked", async () => { - await setupProject(); - - const workflowPath = path.join(tmpDir, PATHS.WORKFLOW_GUIDE_FILE); - const staleWorkflow = - "# Workflow\n\n" + - "## Phase Index\n\n" + - "[workflow-state:in_progress]\nlegacy body\n[/workflow-state:in_progress]\n\n" + - "#### 2.1 Implement `[required · repeatable]`\n\n" + - "[Codex]\nSpawn the implement sub-agent:\n[/Codex]\n\n" + - "[Kilo, Antigravity, Windsurf]\n" + - "1. Load the `trellis-before-dev` skill to read project guidelines\n" + - "[/Kilo, Antigravity, Windsurf]\n"; - - fs.writeFileSync(workflowPath, staleWorkflow, "utf-8"); - - // Simulate an older installed workflow.md that is still pristine relative - // to the version that installed it. Update must replace the whole file: - // platform markers outside [workflow-state:*] blocks are runtime-parsed too. - const hashFile = path.join( - tmpDir, - DIR_NAMES.WORKFLOW, - ".template-hashes.json", - ); - const hashes = readHashesV2(hashFile); - hashes[PATHS.WORKFLOW_GUIDE_FILE] = computeHash(staleWorkflow); - writeHashesV2(hashFile, hashes); - - await update({ force: true }); - - const updated = fs.readFileSync(workflowPath, "utf-8"); - expect(updated).toBe(replacePythonCommandLiterals(workflowMdTemplate)); - expect(updated).toContain( - "[codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae]", - ); - expect(updated).toContain( - "[/Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi]", - ); - expect(updated).toContain("[codex-inline, Kilo, Antigravity, Devin]"); - expect(updated).not.toContain("[Codex]"); - expect(updated).not.toContain("[Kilo, Antigravity, Windsurf]"); - expect(updated).not.toContain("legacy body"); - - expect(readHashesV2(hashFile)[PATHS.WORKFLOW_GUIDE_FILE]).toBe( - computeHash(updated), - ); - }); + expect(readHashesV2(hashFile)[PATHS.WORKFLOW_GUIDE_FILE]).toBe( + computeHash(updated), + ); + }); }); diff --git a/packages/cli/test/configurators/shared.test.ts b/packages/cli/test/configurators/shared.test.ts index 2aa30f01f..5a11f9c47 100644 --- a/packages/cli/test/configurators/shared.test.ts +++ b/packages/cli/test/configurators/shared.test.ts @@ -3,7 +3,6 @@ import { getPythonCommandForPlatform, replacePythonCommandLiterals, resolveAllAsSkillsNeutral, - resolveCommands, resolvePlaceholders, resolvePlaceholdersNeutral, resolveSkillsNeutral, diff --git a/packages/cli/test/templates/trellis.test.ts b/packages/cli/test/templates/trellis.test.ts index 0c3e822a7..3d6d628ba 100644 --- a/packages/cli/test/templates/trellis.test.ts +++ b/packages/cli/test/templates/trellis.test.ts @@ -114,9 +114,9 @@ describe("trellis template constants", () => { for (const script of pyScripts) { expect( script.includes("import") || - script.includes("def ") || - script.includes("class ") || - script.includes("#"), + script.includes("def ") || + script.includes("class ") || + script.includes("#"), ).toBe(true); } }); @@ -177,7 +177,7 @@ describe("trellis template constants", () => { const implement = stepSection("2.1"); const hookAutoBlock = platformBlock( implement, - "[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi]", + "[Claude Code, Cursor, OpenCode, CodeBuddy, Droid, Pi, Oh My Pi]", ); const pullBasedMarker = "[codex-sub-agent, Gemini, Qoder, Copilot, ZCode, Reasonix, Trae]"; From 7545cb24b33e28b5d521422510e44d2127a8f186 Mon Sep 17 00:00:00 2001 From: taosu <taosu@mindfold.ai> Date: Thu, 9 Jul 2026 12:02:57 +0800 Subject: [PATCH 18/19] fix(omp): address extension blocker regressions --- .omp/extensions/trellis/index.ts | 89 ++++++++++++------- .../omp/extensions/trellis/index.ts.txt | 89 ++++++++++++------- packages/cli/test/templates/omp.test.ts | 11 +++ 3 files changed, 125 insertions(+), 64 deletions(-) diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts index e6615c816..c60c1bb55 100644 --- a/.omp/extensions/trellis/index.ts +++ b/.omp/extensions/trellis/index.ts @@ -1,6 +1,6 @@ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { join, dirname } from "node:path"; +import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; +import { join, dirname, isAbsolute, relative, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; @@ -40,13 +40,32 @@ function buildContextKey(platformName: string, kind: string, value: string): str return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`; } -function resolveContextKey(): string | null { - // 1. 显式覆盖 — Trellis hooks 启动子进程时设置 +function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null { + const sessionId = ctx?.sessionManager?.getSessionId?.(); + if (sessionId) { + return buildContextKey("omp", "session", sessionId); + } + const sessionFile = ctx?.sessionManager?.getSessionFile?.(); + if (sessionFile) { + return buildContextKey("omp", "transcript", sessionFile); + } const override = process.env.TRELLIS_CONTEXT_ID?.trim(); - if (override) { - return sanitizeKey(override) || hashValue(override); + return override ? sanitizeKey(override) || hashValue(override) : null; +} + +function isInsideRoot(root: string, candidate: string): boolean { + const rel = relative(root, candidate); + return rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel)); +} + +function resolveProjectFile(projectRoot: string, file: string): string | null { + try { + const rootReal = realpathSync(projectRoot); + const targetReal = realpathSync(resolve(projectRoot, file)); + return isInsideRoot(rootReal, targetReal) ? targetReal : null; + } catch { + return null; } - return null; } // --------------------------------------------------------------------------- @@ -55,12 +74,12 @@ function resolveContextKey(): string | null { function resolveActiveTaskStatus( projectRoot: string, + contextKey: string | null, ): { status: string; taskDir: string | null; taskTitle: string | null } { const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; // --- 通过 context key 解析 session 文件 --- - const contextKey = resolveContextKey(); let sessionFilePath: string | null = null; if (contextKey) { @@ -123,7 +142,7 @@ function resolveActiveTaskStatus( const SESSION_CONTEXT_TIMEOUT_MS = 5000; -function buildSessionContext(projectRoot: string): string { +function buildSessionContext(projectRoot: string, contextKey: string | null): string { const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); if (!existsSync(script)) return ""; @@ -131,6 +150,9 @@ function buildSessionContext(projectRoot: string): string { const result = spawnSync("python3", [script], { cwd: projectRoot, encoding: "utf-8", + env: contextKey + ? { ...process.env, TRELLIS_CONTEXT_ID: contextKey } + : process.env, timeout: SESSION_CONTEXT_TIMEOUT_MS, windowsHide: true, }); @@ -192,8 +214,10 @@ function buildTaskContext(projectRoot: string, taskDir: string, agentType?: Agen const row = JSON.parse(trimmed) as Record<string, unknown>; const file = typeof row.file === "string" ? row.file.trim() : ""; if (!file) continue; + const targetPath = resolveProjectFile(projectRoot, file); + if (!targetPath) continue; let content = ""; - try { content = readFileSync(join(projectRoot, file), "utf-8"); } catch { } + try { content = readFileSync(targetPath, "utf-8"); } catch { } if (content.trim()) { fileChunks.push(`### ${file}\n\n${content.trim()}`); } @@ -226,16 +250,17 @@ class TurnContextCache { private workflowMsg = ""; private static readonly TTL_MS = 1500; - get(projectRoot: string): { workflowMsg: string } { + get(projectRoot: string, contextKey: string | null): { workflowMsg: string } { const now = Date.now(); + const cacheKey = `${projectRoot}:${contextKey ?? ""}`; if ( - this.key === projectRoot && + this.key === cacheKey && now - this.timestamp < TurnContextCache.TTL_MS ) { return { workflowMsg: this.workflowMsg }; } - const { status } = resolveActiveTaskStatus(projectRoot); + const { status } = resolveActiveTaskStatus(projectRoot, contextKey); const workflowPath = join(projectRoot, ".trellis", "workflow.md"); let workflowMd = ""; @@ -255,7 +280,7 @@ class TurnContextCache { this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`; - this.key = projectRoot; + this.key = cacheKey; this.timestamp = now; return { workflowMsg: this.workflowMsg }; } @@ -303,9 +328,8 @@ function detectAgentType(): AgentType { // --------------------------------------------------------------------------- export default function(pi: ExtensionAPI): void { - pi.setLabel("Trellis"); - let projectRoot: string | null = null; + let currentContextKey: string | null = null; const turnCache = new TurnContextCache(); const agentType = detectAgentType(); const isSubAgent = agentType !== null; @@ -315,23 +339,21 @@ export default function(pi: ExtensionAPI): void { let lastCompactionTs = 0; let lastInjectionTs = 0; + const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => { + const key = deriveContextKey(ctx); + if (key) currentContextKey = key; + return currentContextKey; + }; + pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); - // Derive context key from OMP session identity and export it so - // resolveActiveTaskStatus() and spawned subprocesses (get_context.py, - // task.py) can locate the correct session runtime file. - if (!process.env.TRELLIS_CONTEXT_ID) { - const sessionId = (ctx.sessionManager as { getSessionId?: () => string }).getSessionId?.(); - if (sessionId) { - process.env.TRELLIS_CONTEXT_ID = buildContextKey("pi", "session", sessionId); - } - } + const contextKey = rememberContextKey(ctx); if (!projectRoot) return; if (isSubAgent) { // Sub-agent: inject precise task context once - const { taskDir } = resolveActiveTaskStatus(projectRoot); + const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey); if (taskDir) { const taskContext = buildTaskContext(projectRoot, taskDir, agentType); if (taskContext) { @@ -344,7 +366,7 @@ export default function(pi: ExtensionAPI): void { } } else { // Main session: inject session context (global map) + task context - const sessionContext = buildSessionContext(projectRoot); + const sessionContext = buildSessionContext(projectRoot, contextKey); if (sessionContext) { await pi.sendMessage({ customType: "trellis-session-context", @@ -353,7 +375,7 @@ export default function(pi: ExtensionAPI): void { }); } - const { taskDir } = resolveActiveTaskStatus(projectRoot); + const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey); if (taskDir) { const taskContext = buildTaskContext(projectRoot, taskDir); if (taskContext) { @@ -378,9 +400,10 @@ export default function(pi: ExtensionAPI): void { projectRoot = findProjectRoot(ctx.cwd); } if (!projectRoot) return; + const contextKey = rememberContextKey(ctx); // Persistent injection: workflow state for this turn - const cached = turnCache.get(projectRoot); + const cached = turnCache.get(projectRoot, contextKey); lastInjectionTs = Date.now(); return { @@ -395,13 +418,14 @@ export default function(pi: ExtensionAPI): void { // context fires before EVERY LLM API call (including tool-use continuations // and post-compaction agent.continue() paths). Acts as a safety net when // before_agent_start's persisted message was removed by compaction. - pi.on("context", async (event) => { + pi.on("context", async (event, ctx) => { if (!projectRoot) return; + const contextKey = rememberContextKey(ctx); // Fast path: no compaction since last injection — message is still present if (lastInjectionTs > lastCompactionTs) return; - const cached = turnCache.get(projectRoot); + const cached = turnCache.get(projectRoot, contextKey); if (!cached.workflowMsg) return; // Post-compaction: reverse-scan to confirm absence before injecting @@ -433,8 +457,9 @@ export default function(pi: ExtensionAPI): void { } // Resolve projectRoot on first input if session_start missed it if (!projectRoot) return { action: "continue" }; + const contextKey = rememberContextKey(ctx); // Pre-warm the cache so before_agent_start and context can use it - turnCache.get(projectRoot); + turnCache.get(projectRoot, contextKey); return { action: "continue" }; }); } diff --git a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt index e6615c816..c60c1bb55 100644 --- a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -1,6 +1,6 @@ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { join, dirname } from "node:path"; +import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; +import { join, dirname, isAbsolute, relative, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; @@ -40,13 +40,32 @@ function buildContextKey(platformName: string, kind: string, value: string): str return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`; } -function resolveContextKey(): string | null { - // 1. 显式覆盖 — Trellis hooks 启动子进程时设置 +function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null { + const sessionId = ctx?.sessionManager?.getSessionId?.(); + if (sessionId) { + return buildContextKey("omp", "session", sessionId); + } + const sessionFile = ctx?.sessionManager?.getSessionFile?.(); + if (sessionFile) { + return buildContextKey("omp", "transcript", sessionFile); + } const override = process.env.TRELLIS_CONTEXT_ID?.trim(); - if (override) { - return sanitizeKey(override) || hashValue(override); + return override ? sanitizeKey(override) || hashValue(override) : null; +} + +function isInsideRoot(root: string, candidate: string): boolean { + const rel = relative(root, candidate); + return rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel)); +} + +function resolveProjectFile(projectRoot: string, file: string): string | null { + try { + const rootReal = realpathSync(projectRoot); + const targetReal = realpathSync(resolve(projectRoot, file)); + return isInsideRoot(rootReal, targetReal) ? targetReal : null; + } catch { + return null; } - return null; } // --------------------------------------------------------------------------- @@ -55,12 +74,12 @@ function resolveContextKey(): string | null { function resolveActiveTaskStatus( projectRoot: string, + contextKey: string | null, ): { status: string; taskDir: string | null; taskTitle: string | null } { const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions"); if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null }; // --- 通过 context key 解析 session 文件 --- - const contextKey = resolveContextKey(); let sessionFilePath: string | null = null; if (contextKey) { @@ -123,7 +142,7 @@ function resolveActiveTaskStatus( const SESSION_CONTEXT_TIMEOUT_MS = 5000; -function buildSessionContext(projectRoot: string): string { +function buildSessionContext(projectRoot: string, contextKey: string | null): string { const script = join(projectRoot, ".trellis", "scripts", "get_context.py"); if (!existsSync(script)) return ""; @@ -131,6 +150,9 @@ function buildSessionContext(projectRoot: string): string { const result = spawnSync("python3", [script], { cwd: projectRoot, encoding: "utf-8", + env: contextKey + ? { ...process.env, TRELLIS_CONTEXT_ID: contextKey } + : process.env, timeout: SESSION_CONTEXT_TIMEOUT_MS, windowsHide: true, }); @@ -192,8 +214,10 @@ function buildTaskContext(projectRoot: string, taskDir: string, agentType?: Agen const row = JSON.parse(trimmed) as Record<string, unknown>; const file = typeof row.file === "string" ? row.file.trim() : ""; if (!file) continue; + const targetPath = resolveProjectFile(projectRoot, file); + if (!targetPath) continue; let content = ""; - try { content = readFileSync(join(projectRoot, file), "utf-8"); } catch { } + try { content = readFileSync(targetPath, "utf-8"); } catch { } if (content.trim()) { fileChunks.push(`### ${file}\n\n${content.trim()}`); } @@ -226,16 +250,17 @@ class TurnContextCache { private workflowMsg = ""; private static readonly TTL_MS = 1500; - get(projectRoot: string): { workflowMsg: string } { + get(projectRoot: string, contextKey: string | null): { workflowMsg: string } { const now = Date.now(); + const cacheKey = `${projectRoot}:${contextKey ?? ""}`; if ( - this.key === projectRoot && + this.key === cacheKey && now - this.timestamp < TurnContextCache.TTL_MS ) { return { workflowMsg: this.workflowMsg }; } - const { status } = resolveActiveTaskStatus(projectRoot); + const { status } = resolveActiveTaskStatus(projectRoot, contextKey); const workflowPath = join(projectRoot, ".trellis", "workflow.md"); let workflowMd = ""; @@ -255,7 +280,7 @@ class TurnContextCache { this.workflowMsg = `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`; - this.key = projectRoot; + this.key = cacheKey; this.timestamp = now; return { workflowMsg: this.workflowMsg }; } @@ -303,9 +328,8 @@ function detectAgentType(): AgentType { // --------------------------------------------------------------------------- export default function(pi: ExtensionAPI): void { - pi.setLabel("Trellis"); - let projectRoot: string | null = null; + let currentContextKey: string | null = null; const turnCache = new TurnContextCache(); const agentType = detectAgentType(); const isSubAgent = agentType !== null; @@ -315,23 +339,21 @@ export default function(pi: ExtensionAPI): void { let lastCompactionTs = 0; let lastInjectionTs = 0; + const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => { + const key = deriveContextKey(ctx); + if (key) currentContextKey = key; + return currentContextKey; + }; + pi.on("session_start", async (_event, ctx) => { projectRoot = findProjectRoot(ctx.cwd); - // Derive context key from OMP session identity and export it so - // resolveActiveTaskStatus() and spawned subprocesses (get_context.py, - // task.py) can locate the correct session runtime file. - if (!process.env.TRELLIS_CONTEXT_ID) { - const sessionId = (ctx.sessionManager as { getSessionId?: () => string }).getSessionId?.(); - if (sessionId) { - process.env.TRELLIS_CONTEXT_ID = buildContextKey("pi", "session", sessionId); - } - } + const contextKey = rememberContextKey(ctx); if (!projectRoot) return; if (isSubAgent) { // Sub-agent: inject precise task context once - const { taskDir } = resolveActiveTaskStatus(projectRoot); + const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey); if (taskDir) { const taskContext = buildTaskContext(projectRoot, taskDir, agentType); if (taskContext) { @@ -344,7 +366,7 @@ export default function(pi: ExtensionAPI): void { } } else { // Main session: inject session context (global map) + task context - const sessionContext = buildSessionContext(projectRoot); + const sessionContext = buildSessionContext(projectRoot, contextKey); if (sessionContext) { await pi.sendMessage({ customType: "trellis-session-context", @@ -353,7 +375,7 @@ export default function(pi: ExtensionAPI): void { }); } - const { taskDir } = resolveActiveTaskStatus(projectRoot); + const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey); if (taskDir) { const taskContext = buildTaskContext(projectRoot, taskDir); if (taskContext) { @@ -378,9 +400,10 @@ export default function(pi: ExtensionAPI): void { projectRoot = findProjectRoot(ctx.cwd); } if (!projectRoot) return; + const contextKey = rememberContextKey(ctx); // Persistent injection: workflow state for this turn - const cached = turnCache.get(projectRoot); + const cached = turnCache.get(projectRoot, contextKey); lastInjectionTs = Date.now(); return { @@ -395,13 +418,14 @@ export default function(pi: ExtensionAPI): void { // context fires before EVERY LLM API call (including tool-use continuations // and post-compaction agent.continue() paths). Acts as a safety net when // before_agent_start's persisted message was removed by compaction. - pi.on("context", async (event) => { + pi.on("context", async (event, ctx) => { if (!projectRoot) return; + const contextKey = rememberContextKey(ctx); // Fast path: no compaction since last injection — message is still present if (lastInjectionTs > lastCompactionTs) return; - const cached = turnCache.get(projectRoot); + const cached = turnCache.get(projectRoot, contextKey); if (!cached.workflowMsg) return; // Post-compaction: reverse-scan to confirm absence before injecting @@ -433,8 +457,9 @@ export default function(pi: ExtensionAPI): void { } // Resolve projectRoot on first input if session_start missed it if (!projectRoot) return { action: "continue" }; + const contextKey = rememberContextKey(ctx); // Pre-warm the cache so before_agent_start and context can use it - turnCache.get(projectRoot); + turnCache.get(projectRoot, contextKey); return { action: "continue" }; }); } diff --git a/packages/cli/test/templates/omp.test.ts b/packages/cli/test/templates/omp.test.ts index 659ed656e..ae32dbdec 100644 --- a/packages/cli/test/templates/omp.test.ts +++ b/packages/cli/test/templates/omp.test.ts @@ -41,6 +41,17 @@ describe("omp templates", () => { expect(extension).toContain("ExtensionAPI"); }); + it("extension template avoids known runtime and context-safety regressions", () => { + const extension = getExtensionTemplate(); + + expect(extension).not.toContain("pi.setLabel("); + expect(extension).not.toContain("process.env.TRELLIS_CONTEXT_ID ="); + expect(extension).toContain('buildContextKey("omp", "session", sessionId)'); + expect(extension).toContain("realpathSync"); + expect(extension).toContain("resolveProjectFile(projectRoot, file)"); + expect(extension).toContain("readFileSync(targetPath"); + }); + it("extension template contains session context injection markers", () => { const extension = getExtensionTemplate(); // R1: Session start rich injection via get_context.py From 1fbcd9b5a9791f1ff69eba16b9a78d2a810adb98 Mon Sep 17 00:00:00 2001 From: taosu <taosu@mindfold.ai> Date: Thu, 9 Jul 2026 13:34:38 +0800 Subject: [PATCH 19/19] fix(omp): avoid stale session identity fallback --- .omp/extensions/trellis/index.ts | 15 ++++++--------- .../templates/omp/extensions/trellis/index.ts.txt | 15 ++++++--------- packages/cli/test/templates/omp.test.ts | 12 ++++++++++++ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.omp/extensions/trellis/index.ts b/.omp/extensions/trellis/index.ts index c60c1bb55..fb1f4328b 100644 --- a/.omp/extensions/trellis/index.ts +++ b/.omp/extensions/trellis/index.ts @@ -83,15 +83,14 @@ function resolveActiveTaskStatus( let sessionFilePath: string | null = null; if (contextKey) { - // 有身份标识:直接定位文件 const candidate = join(sessionsDir, `${contextKey}.json`); if (existsSync(candidate)) { sessionFilePath = candidate; + } else { + return { status: "no_task", taskDir: null, taskTitle: null }; } - } - - if (!sessionFilePath) { - // 单 session fallback:恰好 1 个文件时使用,否则拒绝猜测 + } else { + // No identity: use single-session fallback only when there is exactly one session file. let sessionFiles: string[]; try { sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); @@ -101,7 +100,6 @@ function resolveActiveTaskStatus( if (sessionFiles.length === 1) { sessionFilePath = join(sessionsDir, sessionFiles[0]); } else { - // 0 或 ≥2 个文件且无身份标识:返回 no_task,不跨会话猜测 return { status: "no_task", taskDir: null, taskTitle: null }; } } @@ -329,7 +327,6 @@ function detectAgentType(): AgentType { export default function(pi: ExtensionAPI): void { let projectRoot: string | null = null; - let currentContextKey: string | null = null; const turnCache = new TurnContextCache(); const agentType = detectAgentType(); const isSubAgent = agentType !== null; @@ -341,8 +338,8 @@ export default function(pi: ExtensionAPI): void { const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => { const key = deriveContextKey(ctx); - if (key) currentContextKey = key; - return currentContextKey; + if (!key) return null; + return key; }; pi.on("session_start", async (_event, ctx) => { diff --git a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt index c60c1bb55..fb1f4328b 100644 --- a/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt +++ b/packages/cli/src/templates/omp/extensions/trellis/index.ts.txt @@ -83,15 +83,14 @@ function resolveActiveTaskStatus( let sessionFilePath: string | null = null; if (contextKey) { - // 有身份标识:直接定位文件 const candidate = join(sessionsDir, `${contextKey}.json`); if (existsSync(candidate)) { sessionFilePath = candidate; + } else { + return { status: "no_task", taskDir: null, taskTitle: null }; } - } - - if (!sessionFilePath) { - // 单 session fallback:恰好 1 个文件时使用,否则拒绝猜测 + } else { + // No identity: use single-session fallback only when there is exactly one session file. let sessionFiles: string[]; try { sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")); @@ -101,7 +100,6 @@ function resolveActiveTaskStatus( if (sessionFiles.length === 1) { sessionFilePath = join(sessionsDir, sessionFiles[0]); } else { - // 0 或 ≥2 个文件且无身份标识:返回 no_task,不跨会话猜测 return { status: "no_task", taskDir: null, taskTitle: null }; } } @@ -329,7 +327,6 @@ function detectAgentType(): AgentType { export default function(pi: ExtensionAPI): void { let projectRoot: string | null = null; - let currentContextKey: string | null = null; const turnCache = new TurnContextCache(); const agentType = detectAgentType(); const isSubAgent = agentType !== null; @@ -341,8 +338,8 @@ export default function(pi: ExtensionAPI): void { const rememberContextKey = (ctx?: { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string } }): string | null => { const key = deriveContextKey(ctx); - if (key) currentContextKey = key; - return currentContextKey; + if (!key) return null; + return key; }; pi.on("session_start", async (_event, ctx) => { diff --git a/packages/cli/test/templates/omp.test.ts b/packages/cli/test/templates/omp.test.ts index ae32dbdec..7ead0ca62 100644 --- a/packages/cli/test/templates/omp.test.ts +++ b/packages/cli/test/templates/omp.test.ts @@ -50,6 +50,18 @@ describe("omp templates", () => { expect(extension).toContain("realpathSync"); expect(extension).toContain("resolveProjectFile(projectRoot, file)"); expect(extension).toContain("readFileSync(targetPath"); + expect(extension).toContain("if (!key) return null;"); + expect(extension).toContain("return key;"); + expect(extension).toContain(`if (existsSync(candidate)) { + sessionFilePath = candidate; + } else { + return { status: "no_task", taskDir: null, taskTitle: null }; + } + } else {`); + expect(extension).toContain( + "No identity: use single-session fallback only when there is exactly one session file.", + ); + expect(extension).not.toContain("currentContextKey"); }); it("extension template contains session context injection markers", () => {