From f22165d29eb5881647fda2a482a3d9a4206ae9fe Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Mon, 29 Jun 2026 23:07:54 +0800 Subject: [PATCH] chore: gitignore + untrack AI agent tooling (.claude/.opencode/.trellis/AGENTS.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这些是本地 AI 助手 / 工作流工具产物,不应进版本库。加入 .gitignore 并 git rm --cached 取消跟踪(本地文件保留)。.omo/ 此前已忽略。 推送后远程仓库一并删除这些路径。 --- .claude/agents/trellis-check.md | 109 --- .claude/agents/trellis-implement.md | 109 --- .claude/agents/trellis-research.md | 137 --- .claude/commands/trellis/continue.md | 55 -- .claude/commands/trellis/finish-work.md | 66 -- .claude/hooks/inject-subagent-context.py | 749 ---------------- .claude/hooks/inject-workflow-state.py | 387 --------- .claude/hooks/session-start.py | 797 ----------------- .claude/plans/bram_migration_plan.md | 80 -- .claude/plans/decaps_plan.md | 89 -- .claude/plans/encaps_plan.md | 90 -- .claude/plans/keygen_plan.md | 175 ---- .claude/plans/phase2_polymem_bram.md | 47 - .claude/settings.json | 73 -- .claude/skills/trellis-before-dev/SKILL.md | 34 - .claude/skills/trellis-brainstorm/SKILL.md | 548 ------------ .claude/skills/trellis-break-loop/SKILL.md | 130 --- .claude/skills/trellis-check/SKILL.md | 92 -- .claude/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 | 74 -- .../platform-files/skills-and-commands.md | 83 -- .../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 -- .claude/skills/trellis-update-spec/SKILL.md | 356 -------- .gitignore | 6 + .opencode/agents/trellis-check.md | 116 --- .opencode/agents/trellis-implement.md | 117 --- .opencode/agents/trellis-research.md | 145 ---- .opencode/commands/trellis/continue.md | 55 -- .opencode/commands/trellis/finish-work.md | 66 -- .opencode/lib/session-utils.js | 432 --------- .opencode/lib/trellis-context.js | 381 -------- .opencode/package.json | 5 - .opencode/plugins/inject-subagent-context.js | 497 ----------- .opencode/plugins/inject-workflow-state.js | 162 ---- .opencode/plugins/session-start.js | 101 --- .opencode/skills/trellis-before-dev/SKILL.md | 34 - .opencode/skills/trellis-brainstorm/SKILL.md | 548 ------------ .opencode/skills/trellis-break-loop/SKILL.md | 130 --- .opencode/skills/trellis-check/SKILL.md | 92 -- .opencode/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 | 74 -- .../platform-files/skills-and-commands.md | 83 -- .../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 -- .opencode/skills/trellis-update-spec/SKILL.md | 356 -------- .trellis/.gitignore | 32 - .trellis/.template-hashes.json | 150 ---- .trellis/.version | 1 - .trellis/config.yaml | 90 -- .trellis/scripts/__init__.py | 5 - .trellis/scripts/add_session.py | 547 ------------ .trellis/scripts/common/__init__.py | 92 -- .trellis/scripts/common/active_task.py | 626 ------------- .trellis/scripts/common/cli_adapter.py | 811 ----------------- .trellis/scripts/common/config.py | 445 ---------- .trellis/scripts/common/developer.py | 190 ---- .trellis/scripts/common/git.py | 31 - .trellis/scripts/common/git_context.py | 106 --- .trellis/scripts/common/io.py | 37 - .trellis/scripts/common/log.py | 45 - .trellis/scripts/common/packages_context.py | 238 ----- .trellis/scripts/common/paths.py | 447 ---------- .trellis/scripts/common/safe_commit.py | 285 ------ .trellis/scripts/common/session_context.py | 821 ------------------ .trellis/scripts/common/task_context.py | 223 ----- .trellis/scripts/common/task_queue.py | 188 ---- .trellis/scripts/common/task_store.py | 697 --------------- .trellis/scripts/common/task_utils.py | 274 ------ .trellis/scripts/common/tasks.py | 112 --- .trellis/scripts/common/trellis_config.py | 131 --- .trellis/scripts/common/types.py | 110 --- .trellis/scripts/common/workflow_phase.py | 215 ----- .trellis/scripts/get_context.py | 16 - .trellis/scripts/get_developer.py | 26 - .trellis/scripts/hooks/linear_sync.py | 243 ------ .trellis/scripts/init_developer.py | 51 -- .trellis/scripts/task.py | 500 ----------- .trellis/spec/backend/database-guidelines.md | 51 -- .trellis/spec/backend/directory-structure.md | 54 -- .trellis/spec/backend/error-handling.md | 51 -- .trellis/spec/backend/index.md | 38 - .trellis/spec/backend/logging-guidelines.md | 51 -- .trellis/spec/backend/quality-guidelines.md | 51 -- .../spec/frontend/component-guidelines.md | 59 -- .trellis/spec/frontend/directory-structure.md | 54 -- .trellis/spec/frontend/hook-guidelines.md | 51 -- .trellis/spec/frontend/index.md | 39 - .trellis/spec/frontend/quality-guidelines.md | 51 -- .trellis/spec/frontend/state-management.md | 51 -- .trellis/spec/frontend/type-safety.md | 51 -- .../spec/guides/code-reuse-thinking-guide.md | 105 --- .../spec/guides/cross-layer-thinking-guide.md | 162 ---- .trellis/spec/guides/index.md | 79 -- .trellis/spec/rtl/index.md | 12 - .trellis/spec/rtl/xsim-tb-conventions.md | 209 ----- .trellis/tasks/00-bootstrap-guidelines/prd.md | 139 --- .../tasks/00-bootstrap-guidelines/task.json | 29 - .../check.jsonl | 1 - .../implement.jsonl | 1 - .../06-27-sha3-g-test-specific-input/prd.md | 30 - .../task.json | 26 - .../2026-06/06-25-fix-tb-failures/check.jsonl | 1 - .../06-25-fix-tb-failures/implement.jsonl | 1 - .../2026-06/06-25-fix-tb-failures/prd.md | 39 - .../2026-06/06-25-fix-tb-failures/task.json | 26 - .../06-25-vivado-verilog-tb/check.jsonl | 1 - .../06-25-vivado-verilog-tb/implement.jsonl | 1 - .../2026-06/06-25-vivado-verilog-tb/prd.md | 167 ---- .../2026-06/06-25-vivado-verilog-tb/task.json | 26 - .../06-26-mlkem-top-integration/check.jsonl | 2 - .../implement.jsonl | 3 - .../06-26-mlkem-top-integration/prd.md | 99 --- .../06-26-mlkem-top-integration/task.json | 26 - .../2026-06/06-27-fix-kg-compute/check.jsonl | 1 - .../06-27-fix-kg-compute/implement.jsonl | 1 - .../2026-06/06-27-fix-kg-compute/prd.md | 20 - .../2026-06/06-27-fix-kg-compute/task.json | 26 - .../06-27-fix-tb-strict-compare/check.jsonl | 1 - .../implement.jsonl | 1 - .../06-27-fix-tb-strict-compare/prd.md | 10 - .../06-27-fix-tb-strict-compare/task.json | 26 - .../06-27-kg-en-de-separate-tb/check.jsonl | 1 - .../implement.jsonl | 1 - .../2026-06/06-27-kg-en-de-separate-tb/prd.md | 18 - .../06-27-kg-en-de-separate-tb/task.json | 26 - .../2026-06/06-27-mlkem-top-tb/check.jsonl | 2 - .../06-27-mlkem-top-tb/implement.jsonl | 2 - .../archive/2026-06/06-27-mlkem-top-tb/prd.md | 50 -- .../2026-06/06-27-mlkem-top-tb/task.json | 26 - .../06-27-vivado-project-tcl/check.jsonl | 1 - .../06-27-vivado-project-tcl/implement.jsonl | 1 - .../2026-06/06-27-vivado-project-tcl/prd.md | 11 - .../06-27-vivado-project-tcl/task.json | 26 - .trellis/workflow.md | 690 --------------- .trellis/workspace/FallenSigh/index.md | 42 - .trellis/workspace/FallenSigh/journal-1.md | 76 -- .trellis/workspace/index.md | 125 --- AGENTS.md | 21 - 183 files changed, 6 insertions(+), 22089 deletions(-) delete mode 100644 .claude/agents/trellis-check.md delete mode 100644 .claude/agents/trellis-implement.md delete mode 100644 .claude/agents/trellis-research.md delete mode 100644 .claude/commands/trellis/continue.md delete mode 100644 .claude/commands/trellis/finish-work.md delete mode 100644 .claude/hooks/inject-subagent-context.py delete mode 100644 .claude/hooks/inject-workflow-state.py delete mode 100644 .claude/hooks/session-start.py delete mode 100644 .claude/plans/bram_migration_plan.md delete mode 100644 .claude/plans/decaps_plan.md delete mode 100644 .claude/plans/encaps_plan.md delete mode 100644 .claude/plans/keygen_plan.md delete mode 100644 .claude/plans/phase2_polymem_bram.md delete mode 100644 .claude/settings.json delete mode 100644 .claude/skills/trellis-before-dev/SKILL.md delete mode 100644 .claude/skills/trellis-brainstorm/SKILL.md delete mode 100644 .claude/skills/trellis-break-loop/SKILL.md delete mode 100644 .claude/skills/trellis-check/SKILL.md delete mode 100644 .claude/skills/trellis-meta/SKILL.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-agents.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-context-loading.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-hooks.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-spec-structure.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/change-workflow.md delete mode 100644 .claude/skills/trellis-meta/references/customize-local/overview.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/context-injection.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/generated-files.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/overview.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/spec-system.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/task-system.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/workflow.md delete mode 100644 .claude/skills/trellis-meta/references/local-architecture/workspace-memory.md delete mode 100644 .claude/skills/trellis-meta/references/platform-files/agents.md delete mode 100644 .claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md delete mode 100644 .claude/skills/trellis-meta/references/platform-files/overview.md delete mode 100644 .claude/skills/trellis-meta/references/platform-files/platform-map.md delete mode 100644 .claude/skills/trellis-meta/references/platform-files/skills-and-commands.md delete mode 100644 .claude/skills/trellis-spec-bootstarp/SKILL.md delete mode 100644 .claude/skills/trellis-spec-bootstarp/references/mcp-setup.md delete mode 100644 .claude/skills/trellis-spec-bootstarp/references/repository-analysis.md delete mode 100644 .claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md delete mode 100644 .claude/skills/trellis-spec-bootstarp/references/spec-writing.md delete mode 100644 .claude/skills/trellis-update-spec/SKILL.md delete mode 100644 .opencode/agents/trellis-check.md delete mode 100644 .opencode/agents/trellis-implement.md delete mode 100644 .opencode/agents/trellis-research.md delete mode 100644 .opencode/commands/trellis/continue.md delete mode 100644 .opencode/commands/trellis/finish-work.md delete mode 100644 .opencode/lib/session-utils.js delete mode 100644 .opencode/lib/trellis-context.js delete mode 100644 .opencode/package.json delete mode 100644 .opencode/plugins/inject-subagent-context.js delete mode 100644 .opencode/plugins/inject-workflow-state.js delete mode 100644 .opencode/plugins/session-start.js delete mode 100644 .opencode/skills/trellis-before-dev/SKILL.md delete mode 100644 .opencode/skills/trellis-brainstorm/SKILL.md delete mode 100644 .opencode/skills/trellis-break-loop/SKILL.md delete mode 100644 .opencode/skills/trellis-check/SKILL.md delete mode 100644 .opencode/skills/trellis-meta/SKILL.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/add-project-local-conventions.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-agents.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-context-loading.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-hooks.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-skills-or-commands.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-spec-structure.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-task-lifecycle.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/change-workflow.md delete mode 100644 .opencode/skills/trellis-meta/references/customize-local/overview.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/context-injection.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/generated-files.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/overview.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/spec-system.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/task-system.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/workflow.md delete mode 100644 .opencode/skills/trellis-meta/references/local-architecture/workspace-memory.md delete mode 100644 .opencode/skills/trellis-meta/references/platform-files/agents.md delete mode 100644 .opencode/skills/trellis-meta/references/platform-files/hooks-and-settings.md delete mode 100644 .opencode/skills/trellis-meta/references/platform-files/overview.md delete mode 100644 .opencode/skills/trellis-meta/references/platform-files/platform-map.md delete mode 100644 .opencode/skills/trellis-meta/references/platform-files/skills-and-commands.md delete mode 100644 .opencode/skills/trellis-spec-bootstarp/SKILL.md delete mode 100644 .opencode/skills/trellis-spec-bootstarp/references/mcp-setup.md delete mode 100644 .opencode/skills/trellis-spec-bootstarp/references/repository-analysis.md delete mode 100644 .opencode/skills/trellis-spec-bootstarp/references/spec-task-planning.md delete mode 100644 .opencode/skills/trellis-spec-bootstarp/references/spec-writing.md delete mode 100644 .opencode/skills/trellis-update-spec/SKILL.md delete mode 100644 .trellis/.gitignore delete mode 100644 .trellis/.template-hashes.json delete mode 100644 .trellis/.version delete mode 100644 .trellis/config.yaml delete mode 100755 .trellis/scripts/__init__.py delete mode 100755 .trellis/scripts/add_session.py delete mode 100755 .trellis/scripts/common/__init__.py delete mode 100755 .trellis/scripts/common/active_task.py delete mode 100755 .trellis/scripts/common/cli_adapter.py delete mode 100755 .trellis/scripts/common/config.py delete mode 100755 .trellis/scripts/common/developer.py delete mode 100755 .trellis/scripts/common/git.py delete mode 100755 .trellis/scripts/common/git_context.py delete mode 100755 .trellis/scripts/common/io.py delete mode 100755 .trellis/scripts/common/log.py delete mode 100755 .trellis/scripts/common/packages_context.py delete mode 100755 .trellis/scripts/common/paths.py delete mode 100755 .trellis/scripts/common/safe_commit.py delete mode 100755 .trellis/scripts/common/session_context.py delete mode 100755 .trellis/scripts/common/task_context.py delete mode 100755 .trellis/scripts/common/task_queue.py delete mode 100755 .trellis/scripts/common/task_store.py delete mode 100755 .trellis/scripts/common/task_utils.py delete mode 100755 .trellis/scripts/common/tasks.py delete mode 100755 .trellis/scripts/common/trellis_config.py delete mode 100755 .trellis/scripts/common/types.py delete mode 100755 .trellis/scripts/common/workflow_phase.py delete mode 100755 .trellis/scripts/get_context.py delete mode 100755 .trellis/scripts/get_developer.py delete mode 100755 .trellis/scripts/hooks/linear_sync.py delete mode 100755 .trellis/scripts/init_developer.py delete mode 100755 .trellis/scripts/task.py delete mode 100644 .trellis/spec/backend/database-guidelines.md delete mode 100644 .trellis/spec/backend/directory-structure.md delete mode 100644 .trellis/spec/backend/error-handling.md delete mode 100644 .trellis/spec/backend/index.md delete mode 100644 .trellis/spec/backend/logging-guidelines.md delete mode 100644 .trellis/spec/backend/quality-guidelines.md delete mode 100644 .trellis/spec/frontend/component-guidelines.md delete mode 100644 .trellis/spec/frontend/directory-structure.md delete mode 100644 .trellis/spec/frontend/hook-guidelines.md delete mode 100644 .trellis/spec/frontend/index.md delete mode 100644 .trellis/spec/frontend/quality-guidelines.md delete mode 100644 .trellis/spec/frontend/state-management.md delete mode 100644 .trellis/spec/frontend/type-safety.md delete mode 100644 .trellis/spec/guides/code-reuse-thinking-guide.md delete mode 100644 .trellis/spec/guides/cross-layer-thinking-guide.md delete mode 100644 .trellis/spec/guides/index.md delete mode 100644 .trellis/spec/rtl/index.md delete mode 100644 .trellis/spec/rtl/xsim-tb-conventions.md delete mode 100644 .trellis/tasks/00-bootstrap-guidelines/prd.md delete mode 100644 .trellis/tasks/00-bootstrap-guidelines/task.json delete mode 100644 .trellis/tasks/06-27-sha3-g-test-specific-input/check.jsonl delete mode 100644 .trellis/tasks/06-27-sha3-g-test-specific-input/implement.jsonl delete mode 100644 .trellis/tasks/06-27-sha3-g-test-specific-input/prd.md delete mode 100644 .trellis/tasks/06-27-sha3-g-test-specific-input/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-25-fix-tb-failures/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-25-fix-tb-failures/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-25-fix-tb-failures/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-25-fix-tb-failures/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-kg-compute/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-kg-compute/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-kg-compute/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-kg-compute/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/task.json delete mode 100644 .trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/check.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/implement.jsonl delete mode 100644 .trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/prd.md delete mode 100644 .trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/task.json delete mode 100644 .trellis/workflow.md delete mode 100644 .trellis/workspace/FallenSigh/index.md delete mode 100644 .trellis/workspace/FallenSigh/journal-1.md delete mode 100644 .trellis/workspace/index.md delete mode 100644 AGENTS.md diff --git a/.claude/agents/trellis-check.md b/.claude/agents/trellis-check.md deleted file mode 100644 index 781094b..0000000 --- a/.claude/agents/trellis-check.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: trellis-check -description: | - Code quality check expert. Reviews code changes against specs and self-fixes issues. -tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa ---- -# Check Agent - -You are the Check Agent in the Trellis workflow. - -## Recursion Guard - -You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. - -- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. -- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. -- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. - -## Trellis Context Loading Protocol - -Look for the `` marker in your input above. - -- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the check work directly. -- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: `, then Read `/prd.md` and the spec files listed in `/check.jsonl` yourself before doing the work. - -## Context - -Before checking, read: -- `.trellis/spec/` - Development guidelines -- Pre-commit checklist for quality standards - -## Core Responsibilities - -1. **Get code changes** - Use git diff to get uncommitted code -2. **Check against specs** - Verify code follows guidelines -3. **Self-fix** - Fix issues yourself, not just report them -4. **Run verification** - typecheck and lint - -## Important - -**Fix issues yourself**, don't just report them. - -You have write and edit tools, you can modify code directly. - ---- - -## Workflow - -### Step 1: Get Changes - -```bash -git diff --name-only # List changed files -git diff # View specific changes -``` - -### Step 2: Check Against Specs - -Read relevant specs in `.trellis/spec/` to check code: - -- Does it follow directory structure conventions -- Does it follow naming conventions -- Does it follow code patterns -- Are there missing types -- Are there potential bugs - -### Step 3: Self-Fix - -After finding issues: - -1. Fix the issue directly (use edit tool) -2. Record what was fixed -3. Continue checking other issues - -### Step 4: Run Verification - -Run project's lint and typecheck commands to verify changes. - -If failed, fix issues and re-run. - ---- - -## Report Format - -```markdown -## Self-Check Complete - -### Files Checked - -- src/components/Feature.tsx -- src/hooks/useFeature.ts - -### Issues Found and Fixed - -1. `:` - -2. `:` - - -### Issues Not Fixed - -(If there are issues that cannot be self-fixed, list them here with reasons) - -### Verification Results - -- TypeCheck: Passed -- Lint: Passed - -### Summary - -Checked X files, found Y issues, all fixed. -``` diff --git a/.claude/agents/trellis-implement.md b/.claude/agents/trellis-implement.md deleted file mode 100644 index 432e6fb..0000000 --- a/.claude/agents/trellis-implement.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: trellis-implement -description: | - Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed. -tools: Read, Write, Edit, Bash, Glob, Grep, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa ---- -# Implement Agent - -You are the Implement Agent in the Trellis workflow. - -## Recursion Guard - -You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. - -- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. -- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. -- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. - -## Trellis Context Loading Protocol - -Look for the `` marker in your input above. - -- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly. -- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: `, then Read `/prd.md`, `/info.md` (if it exists), and the spec files listed in `/implement.jsonl` yourself before doing the work. - -## Context - -Before implementing, read: -- `.trellis/workflow.md` - Project workflow -- `.trellis/spec/` - Development guidelines -- Task `prd.md` - Requirements document -- Task `info.md` - Technical design (if exists) - -## Core Responsibilities - -1. **Understand specs** - Read relevant spec files in `.trellis/spec/` -2. **Understand requirements** - Read prd.md and info.md -3. **Implement features** - Write code following specs and design -4. **Self-check** - Ensure code quality -5. **Report results** - Report completion status - -## Forbidden Operations - -**Do NOT execute these git commands:** - -- `git commit` -- `git push` -- `git merge` - ---- - -## Workflow - -### 1. Understand Specs - -Read relevant specs based on task type: - -- Spec layers: `.trellis/spec///` -- Shared guides: `.trellis/spec/guides/` - -### 2. Understand Requirements - -Read the task's prd.md and info.md: - -- What are the core requirements -- Key points of technical design -- Which files to modify/create - -### 3. Implement Features - -- Write code following specs and technical design -- Follow existing code patterns -- Only do what's required, no over-engineering - -### 4. Verify - -Run project's lint and typecheck commands to verify changes. - ---- - -## Report Format - -```markdown -## Implementation Complete - -### Files Modified - -- `src/components/Feature.tsx` - New component -- `src/hooks/useFeature.ts` - New hook - -### Implementation Summary - -1. Created Feature component... -2. Added useFeature hook... - -### Verification Results - -- Lint: Passed -- TypeCheck: Passed -``` - ---- - -## Code Standards - -- Follow existing code patterns -- Don't add unnecessary abstractions -- Only do what's required, no over-engineering -- Keep code readable diff --git a/.claude/agents/trellis-research.md b/.claude/agents/trellis-research.md deleted file mode 100644 index ce9d5f6..0000000 --- a/.claude/agents/trellis-research.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -name: trellis-research -description: | - Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory. -tools: Read, Write, Glob, Grep, Bash, mcp__exa__web_search_exa, mcp__exa__get_code_context_exa, Skill, mcp__chrome-devtools__* ---- -# Research Agent - -You are the Research Agent in the Trellis workflow. - -## Core Principle - -**You do one thing: find, explain, and PERSIST information.** - -Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session. - ---- - -## Core Responsibilities - -1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read) -2. **External Search** — library docs, API references, best practices (web search) -3. **Persist** — write each research topic to `{TASK_DIR}/research/.md` -4. **Report** — return file paths + one-line summaries to the main agent (not full content) - ---- - -## Workflow - -### Step 1: Resolve Current Task - -Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess. - -Ensure `{TASK_DIR}/research/` exists: - -```bash -mkdir -p /research -``` - -### Step 2: Understand Search Request - -Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison). - -### Step 3: Execute Search - -Run independent searches in parallel (Glob + Grep + web) for efficiency. - -### Step 4: Persist Each Topic - -For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/.md`. Use the File Format below. - -### Step 5: Report to Main Agent - -Reply with ONLY: - -- List of files written (paths relative to repo root) -- One-line summary per file -- Any critical caveats that the main agent needs to know right now - -Do NOT paste full research content into the reply. The files are the contract. - ---- - -## Scope Limits (Strict) - -### Write ALLOWED - -- `{TASK_DIR}/research/*.md` — your own output -- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`) - -### Write FORBIDDEN - -- Code files (`src/`, `lib/`, …) -- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead -- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.) -- Other task directories -- Any git operation (commit / push / branch / merge) - -If the user asks you to edit code, decline and suggest spawning `implement` instead. - ---- - -## File Format - -Each `{TASK_DIR}/research/.md` should follow: - -```markdown -# Research: - -- **Query**: -- **Scope**: -- **Date**: - -## Findings - -### Files Found - -| File Path | Description | -|---|---| -| `src/services/xxx.ts` | Main implementation | -| `src/types/xxx.ts` | Type definitions | - -### Code Patterns - - - -### External References - -- [Library X docs](url) — - -### Related Specs - -- `.trellis/spec/xxx.md` — - -## Caveats / Not Found - - -``` - ---- - -## Guidelines - -### DO - -- Provide specific file paths and line numbers -- Quote actual code snippets -- Persist every topic to its own file -- Return file paths in your reply, not the full content -- Mark "not found" explicitly when searches come up empty - -### DON'T - -- Don't write code or modify files outside `{TASK_DIR}/research/` -- Don't guess uncertain info -- Don't paste full research text into the reply (files are the deliverable) -- Don't propose improvements or critique implementation (that's not your role) diff --git a/.claude/commands/trellis/continue.md b/.claude/commands/trellis/continue.md deleted file mode 100644 index 5261d97..0000000 --- a/.claude/commands/trellis/continue.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 claude -``` - -Follow the loaded instructions. After each `[required]` step completes, move to the next. - ---- - -## Reference - -Full workflow, skill routing table, and the DO-NOT-skip table live in `.trellis/workflow.md`. This command is only an entry point — the canonical guidance is there. diff --git a/.claude/commands/trellis/finish-work.md b/.claude/commands/trellis/finish-work.md deleted file mode 100644 index ab751c6..0000000 --- a/.claude/commands/trellis/finish-work.md +++ /dev/null @@ -1,66 +0,0 @@ -# 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/.claude/hooks/inject-subagent-context.py b/.claude/hooks/inject-subagent-context.py deleted file mode 100644 index 57ed903..0000000 --- a/.claude/hooks/inject-subagent-context.py +++ /dev/null @@ -1,749 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Multi-Platform Sub-Agent Context Injection Hook - -Injects task-specific context when sub-agents (implement, check, research) are spawned. - -Core Design Philosophy: -- Hook is responsible for injecting all context, subagent works autonomously with complete info -- Each agent has a dedicated jsonl file defining its context -- No resume needed, no segmentation, behavior controlled by code not prompt - -Trigger: PreToolUse (before Task tool call) - -Context Source: Trellis active task resolver points to task directory -- implement.jsonl - Implement agent dedicated context -- check.jsonl - Check agent dedicated context -- prd.md - Requirements document -- info.md - Technical design -- codex-review-output.txt - Code Review results -""" -from __future__ import annotations - -# IMPORTANT: Suppress all warnings FIRST -import warnings -warnings.filterwarnings("ignore") - -import json -import os -import sys -from pathlib import Path -from typing import Any - -# IMPORTANT: Force stdout to use UTF-8 on Windows -# This fixes UnicodeEncodeError when outputting non-ASCII characters -if sys.platform.startswith("win"): - import io as _io - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] - elif hasattr(sys.stdout, "detach"): - sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr] - - -# ============================================================================= -# Path Constants (change here to rename directories) -# ============================================================================= - -DIR_WORKFLOW = ".trellis" -DIR_SPEC = "spec" -FILE_TASK_JSON = "task.json" - -# ============================================================================= -# Subagent Constants (change here to rename subagent types) -# ============================================================================= - -AGENT_IMPLEMENT = "trellis-implement" -AGENT_CHECK = "trellis-check" -AGENT_RESEARCH = "trellis-research" - -# Agents that require a task directory -AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK) -# All supported agents -AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH) - - -def find_repo_root(start_path: str) -> str | None: - """ - Find git repo root from start_path upwards - - Returns: - Repo root path, or None if not found - """ - current = Path(start_path).resolve() - while current != current.parent: - if (current / ".git").exists(): - return str(current) - current = current.parent - return None - - -def _detect_platform(input_data: dict) -> str | None: - if isinstance(input_data.get("cursor_version"), str): - return "cursor" - env_map = { - "CLAUDE_PROJECT_DIR": "claude", - "CURSOR_PROJECT_DIR": "cursor", - "CODEBUDDY_PROJECT_DIR": "codebuddy", - "FACTORY_PROJECT_DIR": "droid", - "GEMINI_PROJECT_DIR": "gemini", - "QODER_PROJECT_DIR": "qoder", - "KIRO_PROJECT_DIR": "kiro", - "COPILOT_PROJECT_DIR": "copilot", - } - for env_name, platform in env_map.items(): - if os.environ.get(env_name): - return platform - script_parts = set(Path(sys.argv[0]).parts) - if ".claude" in script_parts: - return "claude" - if ".cursor" in script_parts: - return "cursor" - if ".gemini" in script_parts: - return "gemini" - if ".qoder" in script_parts: - return "qoder" - if ".codebuddy" in script_parts: - return "codebuddy" - if ".factory" in script_parts: - return "droid" - if ".kiro" in script_parts: - return "kiro" - return None - - -def get_current_task(repo_root: str, input_data: dict) -> str | None: - """Resolve current task directory through the unified active task resolver.""" - scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - try: - from common.active_task import resolve_active_task # type: ignore[import-not-found] - except Exception: - return None - - active = resolve_active_task( - Path(repo_root), - input_data, - platform=_detect_platform(input_data), - ) - return active.task_path - - -def read_file_content(base_path: str, file_path: str) -> str | None: - """Read file content, return None if file doesn't exist""" - full_path = os.path.join(base_path, file_path) - if os.path.exists(full_path) and os.path.isfile(full_path): - try: - with open(full_path, "r", encoding="utf-8") as f: - return f.read() - except Exception: - return None - return None - - -def read_directory_contents( - base_path: str, dir_path: str, max_files: int = 20 -) -> list[tuple[str, str]]: - """ - Read all .md files in a directory - - Args: - base_path: Base path (usually repo_root) - dir_path: Directory relative path - max_files: Max files to read (prevent huge directories) - - Returns: - [(file_path, content), ...] - """ - full_path = os.path.join(base_path, dir_path) - if not os.path.exists(full_path) or not os.path.isdir(full_path): - return [] - - results = [] - try: - # Only read .md files, sorted by filename - md_files = sorted( - [ - f - for f in os.listdir(full_path) - if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f)) - ] - ) - - for filename in md_files[:max_files]: - file_full_path = os.path.join(full_path, filename) - relative_path = os.path.join(dir_path, filename) - try: - with open(file_full_path, "r", encoding="utf-8") as f: - content = f.read() - results.append((relative_path, content)) - except Exception: - continue - except Exception: - pass - - return results - - -def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]: - """ - Read all file/directory contents referenced in jsonl file - - Schema: - {"file": "path/to/file.md", "reason": "..."} - {"file": "path/to/dir/", "type": "directory", "reason": "..."} - {"_example": "..."} # seed row — skipped (no `file` field) - - Rows without a ``file`` field (e.g. the self-describing seed line written - by ``task.py create`` before the agent has curated entries) are skipped - silently. If the resulting entry list is empty, a stderr warning is - emitted so the operator can debug missing context. - - Returns: - [(path, content), ...] - """ - full_path = os.path.join(base_path, jsonl_path) - if not os.path.exists(full_path): - print( - f"[inject-subagent-context] WARN: {jsonl_path} not found — " - f"sub-agent will receive only prd.md", - file=sys.stderr, - ) - return [] - - results = [] - saw_real_entry = False - try: - with open(full_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line: - continue - try: - item = json.loads(line) - file_path = item.get("file") or item.get("path") - entry_type = item.get("type", "file") - - if not file_path: - # Seed / comment row — skip silently - continue - - saw_real_entry = True - if entry_type == "directory": - # Read all .md files in directory - dir_contents = read_directory_contents(base_path, file_path) - results.extend(dir_contents) - else: - # Read single file - content = read_file_content(base_path, file_path) - if content: - results.append((file_path, content)) - except json.JSONDecodeError: - continue - except Exception: - pass - - if not saw_real_entry: - print( - f"[inject-subagent-context] WARN: {jsonl_path} has no curated " - f"entries (only seed / empty) — sub-agent will receive only " - f"prd.md. See workflow.md Phase 1.3 for curation guidance.", - file=sys.stderr, - ) - - return results - - - - -def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str: - """ - Get context from {agent_type}.jsonl for the specified agent. - Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates). - """ - context_parts = [] - - agent_jsonl = f"{task_dir}/{agent_type}.jsonl" - for file_path, content in read_jsonl_entries(repo_root, agent_jsonl): - context_parts.append(f"=== {file_path} ===\n{content}") - - return "\n\n".join(context_parts) - - -def get_implement_context(repo_root: str, task_dir: str) -> str: - """ - Complete context for Implement Agent - - Read order: - 1. All files in implement.jsonl (dev specs) - 2. prd.md (requirements) - 3. info.md (technical design) - """ - context_parts = [] - - # 1. Read implement.jsonl - base_context = get_agent_context(repo_root, task_dir, "implement") - if base_context: - context_parts.append(base_context) - - # 2. Requirements document - prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") - if prd_content: - context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") - - # 3. Technical design - info_content = read_file_content(repo_root, f"{task_dir}/info.md") - if info_content: - context_parts.append( - f"=== {task_dir}/info.md (Technical Design) ===\n{info_content}" - ) - - return "\n\n".join(context_parts) - - -def get_check_context(repo_root: str, task_dir: str) -> str: - """ - Context for Check Agent: check.jsonl + prd.md - """ - context_parts = [] - - for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"): - context_parts.append(f"=== {file_path} ===\n{content}") - - prd_content = read_file_content(repo_root, f"{task_dir}/prd.md") - if prd_content: - context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}") - - return "\n\n".join(context_parts) - - -def get_finish_context(repo_root: str, task_dir: str) -> str: - """ - Context for Finish phase: reuses check.jsonl + prd.md - (Finish is a final check, same context source.) - """ - return get_check_context(repo_root, task_dir) - - - -def build_implement_prompt(original_prompt: str, context: str) -> str: - """Build complete prompt for Implement""" - return f""" -# Implement Agent Task - -You are the Implement Agent in the Multi-Agent Pipeline. - -## Your Context - -All the information you need has been prepared for you: - -{context} - ---- - -## Your Task - -{original_prompt} - ---- - -## Workflow - -1. **Understand specs** - All dev specs are injected above, understand them -2. **Understand requirements** - Read requirements document and technical design -3. **Implement feature** - Implement following specs and design -4. **Self-check** - Ensure code quality against check specs - -## Important Constraints - -- Do NOT execute git commit, only code modifications -- Follow all dev specs injected above -- Report list of modified/created files when done""" - - -def build_check_prompt(original_prompt: str, context: str) -> str: - """Build complete prompt for Check""" - return f""" -# Check Agent Task - -You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker). - -## Your Context - -All check specs and dev specs you need: - -{context} - ---- - -## Your Task - -{original_prompt} - ---- - -## Workflow - -1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes -2. **Check against specs** - Check item by item against specs above -3. **Self-fix** - Fix issues directly, don't just report -4. **Run verification** - Run project's lint and typecheck commands - -## Important Constraints - -- Fix issues yourself, don't just report -- Must execute complete checklist in check specs -- Pay special attention to impact radius analysis (L1-L5)""" - - -def build_finish_prompt(original_prompt: str, context: str) -> str: - """Build complete prompt for Finish (final check before PR)""" - return f""" -# Finish Agent Task - -You are performing the final check before creating a PR. - -## Your Context - -Finish checklist and requirements: - -{context} - ---- - -## Your Task - -{original_prompt} - ---- - -## Workflow - -1. **Review changes** - Run `git diff --name-only` to see all changed files -2. **Verify requirements** - Check each requirement in prd.md is implemented -3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions - - If new pattern/convention found: read target spec file → update it → update index.md if needed - - If infra/cross-layer change: follow the 7-section mandatory template from update-spec.md - - If pure code fix with no new patterns: skip this step -4. **Run final checks** - Execute lint and typecheck -5. **Confirm ready** - Ensure code is ready for PR - -## Important Constraints - -- You MAY update spec files when gaps are detected (use update-spec.md as guide) -- MUST read the target spec file BEFORE editing (avoid duplicating existing content) -- Do NOT update specs for trivial changes (typos, formatting, obvious fixes) -- If critical CODE issues found, report them clearly (fix specs, not code) -- Verify all acceptance criteria in prd.md are met""" - - - -def get_research_context(repo_root: str, task_dir: str | None) -> str: - """ - Context for Research Agent — project structure overview for spec directories. - - `task_dir` kept for signature parity with get_implement_context / get_check_context - so the dispatcher can call them uniformly. - """ - _ = task_dir - context_parts = [] - - # 1. Project structure overview (dynamically discover spec directories) - spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}" - spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC - - # Build spec tree dynamically - tree_lines = [f"{spec_path}/"] - if spec_root.is_dir(): - pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir()) - for i, pkg_dir in enumerate(pkg_dirs): - is_last = i == len(pkg_dirs) - 1 - prefix = "└── " if is_last else "├── " - layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir()) - layer_info = f" ({', '.join(layers)})" if layers else "" - tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}") - - spec_tree = "\n".join(tree_lines) - - project_structure = f"""## Project Spec Directory Structure - -``` -{spec_tree} -``` - -To get structured package info, run: `python3 ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages` - -## Search Tips - -- Spec files: `{spec_path}/**/*.md` -- Code search: Use Glob and Grep tools -- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa""" - - context_parts.append(project_structure) - - return "\n\n".join(context_parts) - - -def build_research_prompt(original_prompt: str, context: str) -> str: - """Build complete prompt for Research""" - return f"""# Research Agent Task - -You are the Research Agent in the Multi-Agent Pipeline (search researcher). - -## Core Principle - -**You do one thing: find and explain information.** - -You are a documenter, not a reviewer. - -## Project Info - -{context} - ---- - -## Your Task - -{original_prompt} - ---- - -## Workflow - -1. **Understand query** - Determine search type (internal/external) and scope -2. **Plan search** - List search steps for complex queries -3. **Execute search** - Execute multiple independent searches in parallel -4. **Organize results** - Output structured report - -## Search Tools - -| Tool | Purpose | -|------|---------| -| Glob | Search by filename pattern | -| Grep | Search by content | -| Read | Read file content | -| mcp__exa__web_search_exa | External web search | -| mcp__exa__get_code_context_exa | External code/doc search | - -## Strict Boundaries - -**Only allowed**: Describe what exists, where it is, how it works - -**Forbidden** (unless explicitly asked): -- Suggest improvements -- Criticize implementation -- Recommend refactoring -- Modify any files - -## Report Format - -Provide structured search results including: -- List of files found (with paths) -- Code pattern analysis (if applicable) -- Related spec documents -- External references (if any)""" - - -def _string_value(value: Any) -> str: - if isinstance(value, str): - stripped = value.strip() - return stripped - return "" - - -def _extract_subagent_name(value: Any) -> str: - """Extract a sub-agent name from common platform encodings. - - Cursor's native Task args encode custom sub-agents as a protobuf oneof, - which can appear in hook JSON as either ``{"custom": {"name": "..."}}`` - or ``{"type": {"case": "custom", "value": {"name": "..."}}}``. - """ - direct = _string_value(value) - if direct: - return direct - - if not isinstance(value, dict): - return "" - - for key in ("name", "subagent_type_name", "subagentTypeName"): - direct = _string_value(value.get(key)) - if direct: - return direct - - custom = value.get("custom") - if isinstance(custom, dict): - custom_name = _string_value(custom.get("name")) - if custom_name: - return custom_name - - oneof = value.get("type") - if isinstance(oneof, dict): - case_name = _string_value(oneof.get("case")) - if case_name == "custom": - nested_value = oneof.get("value") - if isinstance(nested_value, dict): - custom_name = _string_value(nested_value.get("name")) - if custom_name: - return custom_name - if case_name: - return case_name - - case_name = _string_value(value.get("case")) - if case_name == "custom": - nested_value = value.get("value") - if isinstance(nested_value, dict): - custom_name = _string_value(nested_value.get("name")) - if custom_name: - return custom_name - if case_name: - return case_name - - for agent_name in AGENTS_ALL: - if agent_name in value: - return agent_name - - return "" - - -def _extract_subagent_type(tool_input: dict) -> str: - for key in ( - "subagent_type", - "subagentType", - "subagent_type_name", - "subagentTypeName", - "agent_type", - "agentType", - "name", - ): - agent_name = _extract_subagent_name(tool_input.get(key)) - if agent_name: - return agent_name - return "" - - -def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]: - """Parse hook input across different platform formats. - - Returns (subagent_type, original_prompt, tool_input). - Handles: - - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type - - Cursor: tool_name=Task|Subagent, tool_input.subagent_type - - Copilot CLI: toolName=task (camelCase key, lowercase value) - - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered) - - Kiro: agentSpawn hook, agent_name field at top level - """ - tool_input = input_data.get("tool_input", {}) - - # Standard format: Task/Agent tool with subagent_type - tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "") - if tool_name.lower() in ("task", "agent", "subagent"): - return ( - _extract_subagent_type(tool_input), - tool_input.get("prompt", ""), - tool_input, - ) - - # Kiro: agentSpawn hook passes agent_name at top level - agent_name = input_data.get("agent_name", "") - if agent_name: - return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input - - # Gemini CLI: BeforeTool where tool_name IS the agent name - # (matcher already ensured it's one of our agents) - if tool_name in AGENTS_ALL: - return tool_name, tool_input.get("prompt", ""), tool_input - - # Copilot CLI: toolName field (camelCase), value might be the agent name - tool_name_camel = input_data.get("toolName", "") - if tool_name_camel in AGENTS_ALL: - return tool_name_camel, input_data.get("toolArgs", ""), tool_input - - return "", "", tool_input - - -def main(): - if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": - sys.exit(0) - - try: - input_data = json.load(sys.stdin) - except json.JSONDecodeError: - sys.exit(0) - - subagent_type, original_prompt, tool_input = _parse_hook_input(input_data) - cwd = input_data.get("cwd", os.getcwd()) - - # Only handle subagent types we care about - if subagent_type not in AGENTS_ALL: - sys.exit(0) - - # Find repo root - repo_root = find_repo_root(cwd) - if not repo_root: - sys.exit(0) - - # Get current task directory (research doesn't require it) - task_dir = get_current_task(repo_root, input_data) - - # implement/check need task directory - if subagent_type in AGENTS_REQUIRE_TASK: - if not task_dir: - sys.exit(0) - # Check if task directory exists - task_dir_full = os.path.join(repo_root, task_dir) - if not os.path.exists(task_dir_full): - sys.exit(0) - - # Check for [finish] marker in prompt (check agent with finish context) - is_finish_phase = "[finish]" in original_prompt.lower() - - # Get context and build prompt based on subagent type - if subagent_type == AGENT_IMPLEMENT: - assert task_dir is not None # validated above - context = get_implement_context(repo_root, task_dir) - new_prompt = build_implement_prompt(original_prompt, context) - elif subagent_type == AGENT_CHECK: - assert task_dir is not None # validated above - if is_finish_phase: - # Finish phase: use finish context (lighter, focused on final verification) - context = get_finish_context(repo_root, task_dir) - new_prompt = build_finish_prompt(original_prompt, context) - else: - # Regular check phase: use check context (full specs for self-fix loop) - context = get_check_context(repo_root, task_dir) - new_prompt = build_check_prompt(original_prompt, context) - elif subagent_type == AGENT_RESEARCH: - # Research can work without task directory - context = get_research_context(repo_root, task_dir) - new_prompt = build_research_prompt(original_prompt, context) - else: - sys.exit(0) - - if not context: - sys.exit(0) - - # Return updated input — use a multi-format output that covers all platforms. - # Most platforms ignore unrecognized fields, so we include multiple formats. - # The platform picks whichever fields it understands. - updated = {**tool_input, "prompt": new_prompt} - output = { - # Claude Code / Qoder / CodeBuddy / Droid format - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "updatedInput": updated, - }, - # Cursor format - "permission": "allow", - "updated_input": updated, - # Gemini format - "updatedInput": updated, - } - - print(json.dumps(output, ensure_ascii=False)) - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/.claude/hooks/inject-workflow-state.py b/.claude/hooks/inject-workflow-state.py deleted file mode 100644 index bd2dca6..0000000 --- a/.claude/hooks/inject-workflow-state.py +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env python3 -"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent). - -Runs on every user prompt. Resolves the active task through Trellis' -session-aware active task resolver and emits a short -block reminding the main AI what task is active and its expected flow. - -The emitted ``hookEventName`` field is platform-aware: most hosts expect -``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder / -CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed -its per-turn event to ``BeforeAgent`` and its schema validator rejects the -legacy name. ``_detect_platform`` picks the right value at runtime. -Breadcrumb text is pulled exclusively from workflow.md -[workflow-state:STATUS] tag blocks — workflow.md is the single source of -truth. There are no fallback dicts in this script: when workflow.md is -missing or a tag is absent, the breadcrumb degrades to a generic -"Refer to workflow.md for current step." line so users see (and fix) -the broken state instead of the hook silently masking it. - -Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder, -CodeBuddy, Droid, Gemini, Copilot). Kiro is not wired (no per-turn -hook entry point). Written to each platform's hooks directory via -writeSharedHooks() at init time. - -Silent exit 0 cases (no output): - - No .trellis/ directory found (not a Trellis project) - - task.json malformed or missing status -""" -from __future__ import annotations - -import json -import os -import re -import sys -from pathlib import Path - -# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is -# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) -# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) -# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` -# but applied per-stream so we don't depend on host CLI's command wiring. -if sys.platform.startswith("win"): - import io as _io - for _stream_name in ("stdin", "stdout", "stderr"): - _stream = getattr(sys, _stream_name, None) - if _stream is None: - continue - if hasattr(_stream, "reconfigure"): - try: - _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] - except Exception: - pass - elif hasattr(_stream, "detach"): - try: - setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) - except Exception: - pass -from typing import Optional - - -CODEX_SUB_AGENT_NOTICE = """ -SUB-AGENT NOTICE - READ FIRST IF SPAWNED VIA spawn_agent - -If your parent session spawned you via spawn_agent with an explicit task -message above this hook output, that message is your only job. -- Execute the parent message exactly as written, then return. -- Ignore all Trellis workflow guidance below this notice. -- Do NOT call task.py start, task.py add-context, or task.py archive. -- Do NOT call wait_agent or spawn_agent. -- Do NOT modify .trellis/tasks/* or any other file unless the parent message - explicitly asks for that. - -If you are the main interactive Codex session and the user is typing at the -terminal with no parent agent, use the workflow guidance below normally. -""" - - -# Bootstrap notice for Codex while the session has no active task. Replaces the -# heavyweight SessionStart context injection — instead of pushing 9.5 KB of -# workflow text up front, we just nudge the AI to read the `trellis-start` skill once. -# The nudge keeps showing up while status == "no_task" (cheap text, AI won't -# re-read after the first time). Once a task is created the breadcrumb status -# flips and this notice stops appearing automatically. Sub-agents are warded -# off by the above plus the explicit exemption below. -CODEX_NO_TASK_BOOTSTRAP_NOTICE = """ -You are running in a Trellis-managed Codex session and there is no active task yet. -If you have not already loaded Trellis context this session, read the `trellis-start` skill once: - - $trellis-start - -(equivalent to reading `.agents/skills/trellis-start/SKILL.md` and following its Steps 1-3) - -The skill walks you through workflow.md, dev profile, git status, active tasks, and spec -indexes. Then route the user's request per the A/B/C rules below. - -Sub-agent exemption: if you are a sub-agent (spawned via spawn_agent with a parent task -message), DO NOT read `$trellis-start`. Execute the parent message directly as instructed by the - above. -""" - - -# --------------------------------------------------------------------------- -# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook) -# --------------------------------------------------------------------------- - -def find_trellis_root(start: Path) -> Optional[Path]: - """Walk up from start to find directory containing .trellis/. - - Handles CWD drift: subdirectory launches, monorepo packages, etc. - Returns None if no .trellis/ found (silent no-op). - """ - cur = start.resolve() - while cur != cur.parent: - if (cur / ".trellis").is_dir(): - return cur - cur = cur.parent - return None - - -# --------------------------------------------------------------------------- -# Active task discovery -# --------------------------------------------------------------------------- - -def _detect_platform(input_data: dict) -> str | None: - if isinstance(input_data.get("cursor_version"), str): - return "cursor" - env_map = { - "CLAUDE_PROJECT_DIR": "claude", - "CURSOR_PROJECT_DIR": "cursor", - "CODEBUDDY_PROJECT_DIR": "codebuddy", - "FACTORY_PROJECT_DIR": "droid", - "GEMINI_PROJECT_DIR": "gemini", - "QODER_PROJECT_DIR": "qoder", - "KIRO_PROJECT_DIR": "kiro", - "COPILOT_PROJECT_DIR": "copilot", - } - for env_name, platform in env_map.items(): - if os.environ.get(env_name): - return platform - script_parts = set(Path(sys.argv[0]).parts) - if ".claude" in script_parts: - return "claude" - if ".cursor" in script_parts: - return "cursor" - if ".codex" in script_parts: - return "codex" - if ".gemini" in script_parts: - return "gemini" - if ".qoder" in script_parts: - return "qoder" - if ".codebuddy" in script_parts: - return "codebuddy" - if ".factory" in script_parts: - return "droid" - if ".kiro" in script_parts: - return "kiro" - return None - - -def _resolve_active_task(root: Path, input_data: dict): - scripts_dir = root / ".trellis" / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - from common.active_task import resolve_active_task # type: ignore[import-not-found] - - return resolve_active_task(root, input_data, platform=_detect_platform(input_data)) - - -def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]: - """Return (task_id, status, source) from the current active task.""" - active = _resolve_active_task(root, input_data) - if not active.task_path: - return None - - task_dir = Path(active.task_path) - if not task_dir.is_absolute(): - task_dir = root / task_dir - if active.stale: - return task_dir.name, f"stale_{active.source_type}", active.source - - task_json = task_dir / "task.json" - if not task_json.is_file(): - return None - try: - data = json.loads(task_json.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError): - return None - - task_id = data.get("id") or task_dir.name - status = data.get("status", "") - if not isinstance(status, str) or not status: - return None - return task_id, status, active.source - - -# --------------------------------------------------------------------------- -# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults -# --------------------------------------------------------------------------- - -# Supports STATUS values with letters, digits, underscores, hyphens -# (so "in-review" / "blocked-by-team" work alongside "in_progress"). -_TAG_RE = re.compile( - r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]", - re.DOTALL, -) - -def load_breadcrumbs(root: Path) -> dict[str, str]: - """Parse workflow.md for [workflow-state:STATUS] blocks. - - Returns {status: body_text}. workflow.md is the single source of - truth — there are no fallback dicts in this script. Missing tags - (or a missing/unreadable workflow.md) fall back to a generic line - in build_breadcrumb so users see the broken state and fix - workflow.md, rather than the hook silently masking the issue. - """ - workflow = root / ".trellis" / "workflow.md" - if not workflow.is_file(): - return {} - try: - content = workflow.read_text(encoding="utf-8") - except OSError: - return {} - - result: dict[str, str] = {} - for match in _TAG_RE.finditer(content): - status = match.group(1) - body = match.group(2).strip() - if body: - result[status] = body - return result - - -def _read_trellis_config(root: Path) -> dict: - """Load .trellis/config.yaml via the bundled trellis_config helper. - - The helper lives in .trellis/scripts/common; the hook lives outside the - scripts tree, so we extend sys.path before importing. - """ - scripts_dir = root / ".trellis" / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - try: - from common.trellis_config import read_trellis_config # type: ignore[import-not-found] - except Exception: - return {} - try: - return read_trellis_config(root) - except Exception: - return {} - - -def _codex_mode_banner(config: dict) -> str: - """Emit a `` banner for the additionalContext payload. - - Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to - `inline` when missing or invalid because Codex sub-agents run with - `fork_turns="none"` isolation and can't inherit the parent session's - task context. The banner makes the active mode explicit to Codex AI - per turn, complementing the workflow-state body which is per-status. - Mode tells AI which dispatch protocol to follow; workflow-state tells - AI what step it's at. - """ - mode = "inline" - if isinstance(config, dict): - codex_cfg = config.get("codex") - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode - return f"{mode}" - - -def resolve_breadcrumb_key( - status: str, platform: str | None, config: dict -) -> str: - """Pick the breadcrumb tag key based on Codex dispatch_mode. - - Codex defaults to ``inline`` because sub-agents run with ``fork_turns="none"`` - isolation and can't inherit the parent session's task context. Users can - opt into ``codex.dispatch_mode: sub-agent`` in ``.trellis/config.yaml`` - to use the parallel ``-inline`` tag → ```` flip. Invalid - or missing values fall back to inline. - - Non-codex platforms return the plain status unchanged. - """ - if platform == "codex": - mode = "inline" - if isinstance(config, dict): - codex_cfg = config.get("codex") - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode - return f"{status}-inline" if mode == "inline" else status - return status - - -def build_breadcrumb( - task_id: Optional[str], - status: str, - templates: dict[str, str], - source: str | None = None, - breadcrumb_key: str | None = None, -) -> str: - """Build the ... block. - - - Known status (tag present in workflow.md) → detailed template body - - Unknown status (no tag, or workflow.md missing) → generic - "Refer to workflow.md for current step." line - - `no_task` pseudo-status (task_id is None) → header omits task info - """ - lookup_key = breadcrumb_key or status - body = templates.get(lookup_key) - if body is None and lookup_key != status: - body = templates.get(status) - if body is None: - body = "Refer to workflow.md for current step." - header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})" - if source: - header = f"{header}\nSource: {source}" - return f"\n{header}\n{body}\n" - - -# --------------------------------------------------------------------------- -# Entry -# --------------------------------------------------------------------------- - -def main() -> int: - if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": - return 0 - - try: - data = json.load(sys.stdin) - except (json.JSONDecodeError, ValueError): - data = {} - - cwd_str = data.get("cwd") or os.getcwd() - cwd = Path(cwd_str) - - root = find_trellis_root(cwd) - if root is None: - return 0 # not a Trellis project - - templates = load_breadcrumbs(root) - platform = _detect_platform(data) - config = _read_trellis_config(root) - task = get_active_task(root, data) - if task is None: - # No active task — still emit a breadcrumb nudging AI toward - # trellis-brainstorm + task.py create when user describes real work. - no_task_key = resolve_breadcrumb_key("no_task", platform, config) - breadcrumb = build_breadcrumb( - None, "no_task", templates, breadcrumb_key=no_task_key - ) - else: - task_id, status, source = task - status_key = resolve_breadcrumb_key(status, platform, config) - breadcrumb = build_breadcrumb( - task_id, status, templates, source, breadcrumb_key=status_key - ) - if platform == "codex": - parts: list[str] = [CODEX_SUB_AGENT_NOTICE] - if task is None: - parts.append(CODEX_NO_TASK_BOOTSTRAP_NOTICE) - parts.append(_codex_mode_banner(config)) - parts.append(breadcrumb) - breadcrumb = "\n\n".join(parts) - - # Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is - # named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/ - # Droid/Codex/Copilot) accept the original Claude-style name. - hook_event_name = ( - "BeforeAgent" if platform == "gemini" else "UserPromptSubmit" - ) - - output = { - "hookSpecificOutput": { - "hookEventName": hook_event_name, - "additionalContext": breadcrumb, - } - } - print(json.dumps(output)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.claude/hooks/session-start.py b/.claude/hooks/session-start.py deleted file mode 100644 index 20398eb..0000000 --- a/.claude/hooks/session-start.py +++ /dev/null @@ -1,797 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Session Start Hook - Inject structured context -""" -from __future__ import annotations - -# IMPORTANT: Suppress all warnings FIRST -import warnings -warnings.filterwarnings("ignore") - -import json -import os -import re -import shlex -import subprocess -import sys -from io import StringIO -from pathlib import Path - - -def _normalize_windows_shell_path(path_str: str) -> str: - """Normalize Unix-style shell paths to real Windows paths. - - On Windows, shells like Git Bash / MSYS2 / Cygwin may report paths like - `/d/Users/...` or `/cygdrive/d/Users/...`. `Path.resolve()` will misinterpret - these as `D:/d/Users...` on drive D: (or similar), breaking repo root - detection. - - This function is intentionally conservative: it only rewrites patterns that - unambiguously represent a drive letter mount. - """ - if not isinstance(path_str, str) or not path_str: - return path_str - - # Only relevant on Windows; keep other platforms untouched. - if not sys.platform.startswith("win"): - return path_str - - p = path_str.strip() - - # Already a Windows drive path (C:\... or C:/...) - if re.match(r"^[A-Za-z]:[\/]", p): - return p - - # MSYS/Git-Bash style: /c/Users/... or /d/Work/... - m = re.match(r"^/([A-Za-z])/(.*)", p) - if m: - drive, rest = m.group(1).upper(), m.group(2) - rest = rest.replace('/', '\\') - return f"{drive}:\\{rest}" - - # Cygwin style: /cygdrive/c/Users/... - m = re.match(r"^/cygdrive/([A-Za-z])/(.*)", p) - if m: - drive, rest = m.group(1).upper(), m.group(2) - rest = rest.replace('/', '\\') - return f"{drive}:\\{rest}" - - # WSL mounted drive (sometimes leaked into env): /mnt/c/Users/... - m = re.match(r"^/mnt/([A-Za-z])/(.*)", p) - if m: - drive, rest = m.group(1).upper(), m.group(2) - rest = rest.replace('/', '\\') - return f"{drive}:\\{rest}" - - return path_str - - -FIRST_REPLY_NOTICE = """ -On the first visible assistant reply in this session, begin with exactly one short Chinese sentence: -Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。 -Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session. -""" - -# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is -# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets) -# both in stdin (hook payload from host CLI) and stdout (our emitted blocks) -# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8` -# but applied per-stream so we don't depend on host CLI's command wiring. -if sys.platform.startswith("win"): - import io as _io - for _stream_name in ("stdin", "stdout", "stderr"): - _stream = getattr(sys, _stream_name, None) - if _stream is None: - continue - if hasattr(_stream, "reconfigure"): - try: - _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] - except Exception: - pass - elif hasattr(_stream, "detach"): - try: - setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace")) - except Exception: - pass - - - -def _has_curated_jsonl_entry(jsonl_path: Path) -> bool: - """Return True iff jsonl has at least one row with a ``file`` field. - - A freshly seeded jsonl only contains a ``{"_example": ...}`` row (no - ``file`` key) — that is NOT "ready". Readiness requires at least one - curated entry. Matches the contract used by hook-inject and pull-based - sub-agent context loaders. - """ - try: - for line in jsonl_path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - row = json.loads(line) - except json.JSONDecodeError: - continue - if isinstance(row, dict) and row.get("file"): - return True - except (OSError, UnicodeDecodeError): - return False - return False - - -def should_skip_injection() -> bool: - """Check if any platform's non-interactive flag is set, or if Trellis - hooks are explicitly disabled via TRELLIS_HOOKS=0 / TRELLIS_DISABLE_HOOKS=1. - """ - if os.environ.get("TRELLIS_HOOKS") == "0": - return True - if os.environ.get("TRELLIS_DISABLE_HOOKS") == "1": - return True - non_interactive_vars = [ - "CLAUDE_NON_INTERACTIVE", - "QODER_NON_INTERACTIVE", - "CODEBUDDY_NON_INTERACTIVE", - "FACTORY_NON_INTERACTIVE", - "CURSOR_NON_INTERACTIVE", - "GEMINI_NON_INTERACTIVE", - "KIRO_NON_INTERACTIVE", - "COPILOT_NON_INTERACTIVE", - ] - return any(os.environ.get(var) == "1" for var in non_interactive_vars) - - -def read_file(path: Path, fallback: str = "") -> str: - try: - return path.read_text(encoding="utf-8") - except (FileNotFoundError, PermissionError): - return fallback - - -def _detect_platform(input_data: dict) -> str | None: - if isinstance(input_data.get("cursor_version"), str): - return "cursor" - env_map = { - "CLAUDE_PROJECT_DIR": "claude", - "CURSOR_PROJECT_DIR": "cursor", - "CODEBUDDY_PROJECT_DIR": "codebuddy", - "FACTORY_PROJECT_DIR": "droid", - "GEMINI_PROJECT_DIR": "gemini", - "QODER_PROJECT_DIR": "qoder", - "KIRO_PROJECT_DIR": "kiro", - "COPILOT_PROJECT_DIR": "copilot", - } - for env_name, platform in env_map.items(): - if os.environ.get(env_name): - return platform - script_parts = set(Path(sys.argv[0]).parts) - if ".claude" in script_parts: - return "claude" - if ".cursor" in script_parts: - return "cursor" - if ".codex" in script_parts: - return "codex" - if ".gemini" in script_parts: - return "gemini" - if ".qoder" in script_parts: - return "qoder" - if ".codebuddy" in script_parts: - return "codebuddy" - if ".factory" in script_parts: - return "droid" - if ".kiro" in script_parts: - return "kiro" - return None - - -def _resolve_context_key(trellis_dir: Path, input_data: dict) -> str | None: - scripts_dir = trellis_dir / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - from common.active_task import resolve_context_key # type: ignore[import-not-found] - - return resolve_context_key(input_data, platform=_detect_platform(input_data)) - - -def _persist_context_key_for_bash(context_key: str | None) -> None: - """Expose Trellis session identity to later Claude Code Bash commands. - - Claude Code SessionStart hooks can append exports to CLAUDE_ENV_FILE; those - variables are then available to Bash tools in the same conversation. Without - this bridge, `task.py start` has hook stdin during SessionStart but no - session identity when the AI later runs it as a normal shell command. - """ - if not context_key: - return - env_file = os.environ.get("CLAUDE_ENV_FILE") - if not env_file: - return - try: - with open(env_file, "a", encoding="utf-8") as handle: - handle.write(f"export TRELLIS_CONTEXT_ID={shlex.quote(context_key)}\n") - except OSError: - pass - - -def _resolve_active_task(trellis_dir: Path, input_data: dict): - scripts_dir = trellis_dir / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - from common.active_task import resolve_active_task # type: ignore[import-not-found] - - return resolve_active_task( - trellis_dir.parent, - input_data, - platform=_detect_platform(input_data), - ) - - -def run_script(script_path: Path, context_key: str | None = None) -> str: - try: - if script_path.suffix == ".py": - # Add PYTHONIOENCODING to force UTF-8 in subprocess - env = os.environ.copy() - env["PYTHONIOENCODING"] = "utf-8" - if context_key: - env["TRELLIS_CONTEXT_ID"] = context_key - cmd = [sys.executable, "-W", "ignore", str(script_path)] - else: - env = os.environ.copy() - if context_key: - env["TRELLIS_CONTEXT_ID"] = context_key - cmd = [str(script_path)] - - result = subprocess.run( - cmd, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - timeout=5, - cwd=script_path.parent.parent.parent, - env=env, - ) - return result.stdout if result.returncode == 0 else "No context available" - except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError): - return "No context available" - - -def _normalize_task_ref(task_ref: str) -> str: - normalized = task_ref.strip() - if not normalized: - return "" - - path_obj = Path(normalized) - if path_obj.is_absolute(): - return str(path_obj) - - normalized = normalized.replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - - if normalized.startswith("tasks/"): - return f".trellis/{normalized}" - - return normalized - - -def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path: - normalized = _normalize_task_ref(task_ref) - path_obj = Path(normalized) - if path_obj.is_absolute(): - return path_obj - if normalized.startswith(".trellis/"): - return trellis_dir.parent / path_obj - return trellis_dir / "tasks" / path_obj - - -def _get_task_status(trellis_dir: Path, input_data: dict) -> str: - """Check current task status and return structured status string with explicit next action. - - Returns a block with three fields: - - Status: current state - - Task: task identifier (when applicable) - - Next-Action: explicit skill/command/tool call the AI should invoke - """ - active = _resolve_active_task(trellis_dir, input_data) - - # Case 1: No active task — waiting for user to describe intent - if not active.task_path: - return ( - "Status: NO ACTIVE TASK\n" - f"Source: {active.source}\n" - "Next-Action: After the user describes their intent, load skill `trellis-brainstorm` " - "to clarify requirements and create a task via `python3 ./.trellis/scripts/task.py create`.\n" - "Research reminder: for research-heavy tasks (comparing tools, reading external docs, " - "cross-platform surveys), spawn `trellis-research` sub-agents via the Task tool — " - "they persist findings to `{TASK_DIR}/research/*.md` and keep main context clean. " - "Do NOT do 10+ inline WebFetch/WebSearch in the main conversation.\n" - "User override (per-turn escape hatch): if the user's first message explicitly opts " - "out of the workflow (\"跳过 trellis\" / \"别走流程\" / \"小修一下\" / \"直接改\" / " - "\"skip trellis\" / \"no task\" / \"just do it\"), honor it for this turn — " - "acknowledge briefly and proceed without creating a task. Per-turn only." - ) - - # Case 2: Stale pointer — task dir was deleted - task_ref = active.task_path - task_dir = _resolve_task_dir(trellis_dir, task_ref) - if active.stale or not task_dir.is_dir(): - return ( - f"Status: STALE POINTER\nTask: {task_ref}\n" - f"Source: {active.source}\n" - f"Next-Action: Run `python3 ./.trellis/scripts/task.py finish` to clear the stale pointer, " - "then ask the user what to work on next." - ) - - # Read task.json - task_json_path = task_dir / "task.json" - task_data = {} - if task_json_path.is_file(): - try: - task_data = json.loads(task_json_path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, PermissionError): - pass - - task_title = task_data.get("title", task_ref) - task_status = task_data.get("status", "unknown") - - # Case 3: Task completed — time to archive - if task_status == "completed": - return ( - f"Status: COMPLETED\nTask: {task_title}\n" - f"Source: {active.source}\n" - f"Next-Action: Load skill `trellis-update-spec` to capture learnings, " - f"then archive with `python3 ./.trellis/scripts/task.py archive {task_dir.name}`." - ) - - has_prd = (task_dir / "prd.md").is_file() - - # Case 4: No PRD — still in Plan phase - if not has_prd: - return ( - f"Status: PLANNING\nTask: {task_title}\n" - f"Source: {active.source}\n" - "Next-Action: Load skill `trellis-brainstorm` to clarify requirements with the user " - "and produce prd.md in the task directory.\n" - "Research reminder: when the task needs external research (tool comparison, docs, " - "conventions survey), spawn `trellis-research` sub-agents — don't WebFetch/WebSearch " - "inline in the main session. Findings go to `{task_dir}/research/*.md`; PRD only links to them." - ) - - # Case 4b: PRD exists but implement.jsonl has only seed (no curated entries) — Phase 1.3 gate - implement_jsonl = task_dir / "implement.jsonl" - if implement_jsonl.is_file() and not _has_curated_jsonl_entry(implement_jsonl): - return ( - f"Status: PLANNING (Phase 1.3)\nTask: {task_title}\n" - f"Source: {active.source}\n" - "Next-Action: Curate `implement.jsonl` and `check.jsonl` with the spec + research files " - "the Phase 2 sub-agents will need. Only spec paths (`.trellis/spec/**/*.md`) and research " - "files (`{TASK_DIR}/research/*.md`) — no code paths. Run " - "`python3 ./.trellis/scripts/get_context.py --mode packages` to list available specs, " - "then edit the jsonl files or use `python3 ./.trellis/scripts/task.py add-context`. " - "See `.trellis/workflow.md` Phase 1.3 for details." - ) - - # Case 5: PRD + curated jsonl (or agent-less platform with no jsonl) — enter Execute phase - return ( - f"Status: READY\nTask: {task_title}\n" - f"Source: {active.source}\n" - "Next required action: dispatch `trellis-implement` per Phase 2.1. " - "For agent-capable platforms, the default is to NOT edit code in the main session. " - "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n" - "Sub-agent roster: `trellis-implement` (writes code), `trellis-check` (verifies + self-fixes), " - "`trellis-research` (persists findings to `research/*.md` — use when you'd otherwise do " - "multiple WebFetch/WebSearch inline).\n" - "Sub-agent self-exemption: if you are reading this as a `trellis-implement` or " - "`trellis-check` sub-agent (your own role / agent name reflects that), this dispatch " - "instruction does NOT apply to you — you are already the dispatched sub-agent. " - "Implement / check directly without spawning another sub-agent of the same kind.\n" - "User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the " - "main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / " - "\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. " - "Per-turn only; do NOT invent an override the user did not say." - ) - - -def _load_trellis_config(trellis_dir: Path, input_data: dict) -> tuple: - """Load Trellis config for session-start decisions. - - Returns: - (is_mono, packages_dict, spec_scope, task_pkg, default_pkg) - """ - scripts_dir = trellis_dir / "scripts" - if str(scripts_dir) not in sys.path: - sys.path.insert(0, str(scripts_dir)) - - try: - from common.config import get_default_package, get_packages, get_spec_scope, is_monorepo # type: ignore[import-not-found] - from common.paths import get_current_task # type: ignore[import-not-found] - - repo_root = trellis_dir.parent - is_mono = is_monorepo(repo_root) - packages = get_packages(repo_root) or {} - scope = get_spec_scope(repo_root) - - # Get active task's package - task_pkg = None - current = get_current_task( - repo_root, - input_data, - platform=_detect_platform(input_data), - ) - if current: - task_json = repo_root / current / "task.json" - if task_json.is_file(): - try: - data = json.loads(task_json.read_text(encoding="utf-8")) - if isinstance(data, dict): - tp = data.get("package") - if isinstance(tp, str) and tp: - task_pkg = tp - except (json.JSONDecodeError, OSError): - pass - - default_pkg = get_default_package(repo_root) - return is_mono, packages, scope, task_pkg, default_pkg - except Exception: - return False, {}, None, None, None - - -def _check_legacy_spec(trellis_dir: Path, is_mono: bool, packages: dict) -> str | None: - """Check for legacy spec directory structure in monorepo. - - Returns warning message if legacy structure detected, None otherwise. - """ - if not is_mono or not packages: - return None - - spec_dir = trellis_dir / "spec" - if not spec_dir.is_dir(): - return None - - # Check for legacy flat spec dirs (spec/backend/, spec/frontend/ with index.md) - has_legacy = False - for legacy_name in ("backend", "frontend"): - legacy_dir = spec_dir / legacy_name - if legacy_dir.is_dir() and (legacy_dir / "index.md").is_file(): - has_legacy = True - break - - if not has_legacy: - return None - - # Check which packages are missing spec// directory - missing = [ - name for name in sorted(packages.keys()) - if not (spec_dir / name).is_dir() - ] - - if not missing: - return None # All packages have spec dirs - - if len(missing) == len(packages): - return ( - f"[!] Legacy spec structure detected: found `spec/backend/` or `spec/frontend/` " - f"but no package-scoped `spec//` directories.\n" - f"Monorepo packages: {', '.join(sorted(packages.keys()))}\n" - f"Please reorganize: `spec/backend/` -> `spec//backend/`" - ) - return ( - f"[!] Partial spec migration detected: packages {', '.join(missing)} " - f"still missing `spec//` directory.\n" - f"Please complete migration for all packages." - ) - - -def _resolve_spec_scope( - is_mono: bool, - packages: dict, - scope, - task_pkg: str | None, - default_pkg: str | None, -) -> set | None: - """Resolve which packages should have their specs injected. - - Returns: - Set of package names to include, or None for full scan. - """ - if not is_mono or not packages: - return None # Single-repo: full scan - - if scope is None: - return None # No scope configured: full scan - - if isinstance(scope, str) and scope == "active_task": - if task_pkg and task_pkg in packages: - return {task_pkg} - if default_pkg and default_pkg in packages: - return {default_pkg} - return None # Fallback to full scan - - if isinstance(scope, list): - valid = set() - for entry in scope: - if entry in packages: - valid.add(entry) - else: - print( - f"Warning: spec_scope contains unknown package: {entry}, ignoring", - file=sys.stderr, - ) - - if valid: - # Warn if active task is out of scope - if task_pkg and task_pkg not in valid: - print( - f"Warning: active task package '{task_pkg}' is out of configured spec_scope", - file=sys.stderr, - ) - return valid - - # All entries invalid: fallback chain - print( - "Warning: all spec_scope entries invalid, falling back to task/default/full", - file=sys.stderr, - ) - if task_pkg and task_pkg in packages: - return {task_pkg} - if default_pkg and default_pkg in packages: - return {default_pkg} - return None # Full scan - - return None # Unknown scope type: full scan - - -def _extract_range(content: str, start_header: str, end_header: str) -> str: - """Extract lines starting at `## start_header` up to (but excluding) `## end_header`. - - Both parameters are full header lines WITHOUT the `## ` prefix (e.g. "Phase Index"). - Returns empty string if start header is not found. - End header missing → extracts to end of file. - """ - lines = content.splitlines() - start: int | None = None - end: int = len(lines) - start_match = f"## {start_header}" - end_match = f"## {end_header}" - for i, line in enumerate(lines): - stripped = line.strip() - if start is None and stripped == start_match: - start = i - continue - if start is not None and stripped == end_match: - end = i - break - if start is None: - return "" - return "\n".join(lines[start:end]).rstrip() - - -_BREADCRUMB_TAG_RE = re.compile( - r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]", - re.DOTALL, -) - - -def _strip_breadcrumb_tag_blocks(content: str) -> str: - """Remove `[workflow-state:STATUS]...[/workflow-state:STATUS]` blocks. - - The tag blocks live inside `## Phase Index` (since v0.5.0-rc.0, when - they were colocated with their phase summaries) and are consumed by the - UserPromptSubmit hook (`inject-workflow-state.py`). The session-start - payload already covers the full step bodies, so re-inlining the - breadcrumbs here would just duplicate context. - """ - return _BREADCRUMB_TAG_RE.sub("", content) - - -def _build_workflow_overview(workflow_path: Path) -> str: - """Inject the workflow guide for the session. - - Contents: - 1. Section index (all `## ` headings — navigation) - 2. Phase Index section (rules, skill routing table, anti-rationalization table) - 3. Phase 1/2/3 step-level details (the actual how-to for each step) - - The meta sections (Core Principles / Trellis System / Customizing - Trellis) are NOT injected — Core Principles is short prose the AI can - Read on demand; Trellis System lists reference commands duplicated in - step bodies; Customizing Trellis is for forks. Workflow-state breadcrumb - tag blocks (which now live inside Phase Index since v0.5.0-rc.0) are - stripped from the extracted range — they're consumed by the - UserPromptSubmit hook, not the session-start preamble. - - Total budget: Phase Index ~2 KB + Phase 1/2/3 ~7 KB = ~9 KB. - """ - content = read_file(workflow_path) - if not content: - return "No workflow.md found" - - out_lines = [ - "# Development Workflow — Section Index", - "Full guide: .trellis/workflow.md (read on demand)", - "", - "## Table of Contents", - ] - for line in content.splitlines(): - if line.startswith("## "): - out_lines.append(line) - out_lines += ["", "---", ""] - - # Extract Phase Index through the end of Phase 3 (before "Customizing - # Trellis" — the docs-for-forks footer added in v0.5.0-rc.0). Since - # sections appear in order Phase Index → Phase 1 → Phase 2 → Phase 3 → - # Customizing Trellis, a single range grab captures all four. The - # breadcrumb tag blocks now embedded inside Phase Index are stripped so - # they don't duplicate the per-turn UserPromptSubmit injection. - phases = _extract_range( - content, "Phase Index", "Customizing Trellis (for forks)" - ) - if phases: - out_lines.append(_strip_breadcrumb_tag_blocks(phases).rstrip()) - - return "\n".join(out_lines).rstrip() - - -def main(): - if should_skip_injection(): - sys.exit(0) - - try: - hook_input = json.loads(sys.stdin.read()) - if not isinstance(hook_input, dict): - hook_input = {} - except (json.JSONDecodeError, ValueError): - hook_input = {} - - # Try platform-specific env vars, hook cwd, fallback to cwd - project_dir_env_vars = [ - "CLAUDE_PROJECT_DIR", - "QODER_PROJECT_DIR", - "CODEBUDDY_PROJECT_DIR", - "FACTORY_PROJECT_DIR", - "CURSOR_PROJECT_DIR", - "GEMINI_PROJECT_DIR", - "KIRO_PROJECT_DIR", - "COPILOT_PROJECT_DIR", - ] - project_dir = None - for var in project_dir_env_vars: - val = os.environ.get(var) - if val: - project_dir = Path(_normalize_windows_shell_path(val)).resolve() - break - if project_dir is None: - project_dir = Path(_normalize_windows_shell_path(hook_input.get("cwd", "."))).resolve() - - trellis_dir = project_dir / ".trellis" - context_key = _resolve_context_key(trellis_dir, hook_input) - _persist_context_key_for_bash(context_key) - - # Load config for scope filtering and legacy detection - is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config( - trellis_dir, - hook_input, - ) - allowed_pkgs = _resolve_spec_scope(is_mono, packages, scope_config, task_pkg, default_pkg) - - output = StringIO() - - output.write(""" -You are starting a new session in a Trellis-managed project. -Read and follow all instructions below carefully. - - -""") - output.write(FIRST_REPLY_NOTICE) - output.write("\n\n") - - # Legacy migration warning - legacy_warning = _check_legacy_spec(trellis_dir, is_mono, packages) - if legacy_warning: - output.write(f"\n{legacy_warning}\n\n\n") - - output.write("\n") - context_script = trellis_dir / "scripts" / "get_context.py" - output.write(run_script(context_script, context_key)) - output.write("\n\n\n") - - output.write("\n") - output.write(_build_workflow_overview(trellis_dir / "workflow.md")) - output.write("\n\n\n") - - output.write("\n") - output.write( - "Project spec indexes are listed by path below. Each index contains a " - "**Pre-Development Checklist** listing the specific guideline files to " - "read before coding.\n\n" - "- If you're spawning an implement/check sub-agent, context is injected " - "or loaded by the sub-agent via `{task}/implement.jsonl` / `check.jsonl`. " - "You do NOT need to read these indexes yourself.\n" - "- For agent-capable platforms, the default is to dispatch " - "`trellis-implement` and `trellis-check` (so JSONL context is loaded by " - "the sub-agents) rather than editing code in the main session. " - "Honor a per-turn user override only if the user's current message " - "explicitly opts out (see below for override phrases).\n" - "- Sub-agent self-exemption: if you are reading this as a `trellis-implement` " - "or `trellis-check` sub-agent, the \"dispatch trellis-implement / trellis-check\" " - "rule above does NOT apply to you — you are already the dispatched sub-agent. " - "Do NOT spawn another sub-agent of the same kind; implement / check directly.\n\n" - ) - - # guides/ is cross-package thinking — always include inline (small, broadly useful) - guides_index = trellis_dir / "spec" / "guides" / "index.md" - if guides_index.is_file(): - output.write("## guides (inlined — cross-package thinking guides)\n") - output.write(read_file(guides_index)) - output.write("\n\n") - - # Other spec indexes — paths only (main agent reads on demand; - # sub-agents get their specific specs via jsonl injection) - paths: list[str] = [] - spec_dir = trellis_dir / "spec" - if spec_dir.is_dir(): - for sub in sorted(spec_dir.iterdir()): - if not sub.is_dir() or sub.name.startswith("."): - continue - if sub.name == "guides": - continue # already inlined above - - index_file = sub / "index.md" - if index_file.is_file(): - # Flat spec dir (single-repo layer like spec/backend/) - paths.append(f".trellis/spec/{sub.name}/index.md") - else: - # Nested package dirs (monorepo: spec///index.md) - # Apply scope filter - if allowed_pkgs is not None and sub.name not in allowed_pkgs: - continue - for nested in sorted(sub.iterdir()): - if not nested.is_dir(): - continue - nested_index = nested / "index.md" - if nested_index.is_file(): - paths.append( - f".trellis/spec/{sub.name}/{nested.name}/index.md" - ) - - if paths: - output.write("## Available spec indexes (read on demand)\n") - for p in paths: - output.write(f"- {p}\n") - output.write("\n") - - output.write( - "Discover more via: " - "`python3 ./.trellis/scripts/get_context.py --mode packages`\n" - ) - output.write("\n\n") - - # Check task status and inject structured tag - task_status = _get_task_status(trellis_dir, hook_input) - output.write(f"\n{task_status}\n\n\n") - - output.write(""" -Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them. -When the user sends the first message, follow and the workflow guide. -If a task is READY, execute its Next required action without asking whether to continue. -""") - - context_text = output.getvalue() - result = { - # Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot format - "hookSpecificOutput": { - "hookEventName": "SessionStart", - "additionalContext": context_text, - }, - # Cursor sessionStart format (top-level snake_case per Cursor docs) - "additional_context": context_text, - } - - # Output JSON - stdout is already configured for UTF-8 - print(json.dumps(result, ensure_ascii=False), flush=True) - - -if __name__ == "__main__": - main() diff --git a/.claude/plans/bram_migration_plan.md b/.claude/plans/bram_migration_plan.md deleted file mode 100644 index be6be9e..0000000 --- a/.claude/plans/bram_migration_plan.md +++ /dev/null @@ -1,80 +0,0 @@ -# 计划:将 mlkem_top 的大数据存储迁移到 BRAM - -## 目标 -把当前用裸 `reg` 数组实现的三块大存储改为可被 Vivado 推断为 block RAM 的结构, -使设计面向综合时不再把 86 kbit 的多项式存储映射成 LUT/FF。 - -涉及存储: -| 当前 reg 数组 | 容量(KMAX=4) | 用途 | -|---|---|---| -| `polymem [0:28*256-1]` ×12bit | 86 016 bit | 全部多项式 Â/ŝ/ê/t̂ | -| `ek_mem [0:1567]` ×8bit | 12 544 bit | byteEncode 后 ek | -| `dkp_mem [0:1535]` ×8bit | 12 288 bit | byteEncode 后 dk_pke | - -复用现有 `sync_rtl/storage/sd_bram.v`(1 读 + 1 写,读地址寄存→1 周期读延迟, -W≥12 & D≥64 时 Vivado 自动推断 BRAM)。 - -## 核心约束(已从代码确认) -1. **BRAM 读有 1 周期延迟**(reg 数组是组合读)。所有读出点都要改成“地址提前一拍”。 -2. **BRAM 每端口每周期 ≤1 访问**。当前 `polymem` 在同周期最多有 2 个不同读地址 - (ST_M LOAD 同时读 A 和 ŝ),且 ST_E 同周期读同一 poly 的 2 个系数。 -3. **ST_E 当前每周期写 3 字节**到 ek/dk —— BRAM 单写口做不到。 - -## 关键结论(决定方案可行性) -- `poly_mul_sync` 在 LOAD 阶段把 256 对系数**全部存进自己的 mem_A/mem_B**,再在 COMP - 阶段输出。所以 ST_M 的 LOAD(读 A+ŝ)与 ACCUMULATE(读 ê/t̂、写 t̂)**绝不同周期**。 - → `polymem` 按区域分 4 个 bank {A, S, E, T} 后,每个 bank 任意周期 ≤1 读 + 1 写, - `sd_bram` 即可满足,无需真双口。 -- TB 每次调试回读等 **2 个周期**(`@(posedge clk); @(posedge clk);`),可吸收 BRAM 多出的 - 1 拍读延迟,回读路径无需改 TB。 - -## polymem 4-bank 划分 -| bank | slot 数 | 深度(×256) | 写于 | 读于 | -|---|---|---|---|---| -| A (Â[i][j]) | KMAX²=16 | 4096 | ST_A | ST_M LOAD | -| S (ŝ[i]) | KMAX=4 | 1024 | ST_C / ST_N | ST_N LOAD, ST_M LOAD, ST_E | -| E (ê[i]) | KMAX=4 | 1024 | ST_C / ST_N | ST_N LOAD, ST_M ACC(j=0) | -| T (t̂[i]) | KMAX=4 | 1024 | ST_M ACC | ST_M ACC(j>0), ST_E | -(总深度 7168 == 当前 28×256,容量不变) - -每个 bank 一个 `sd_bram #(.W(12))`。读地址由一个 per-bank mux 选择:计算期由 FSM 计数器驱动, -ST_DONE/回读期由 `dbg_*` 驱动;`dbg_coeff_o` 改为按 slot 选 4 个 bank 的 rd_data。 - -## 实施分两阶段,每阶段后跑全部 11 个 KAT 用例 - -### 阶段 1:ek_mem / dkp_mem → BRAM(风险较低,先做) -1. 例化 `ek_bram`、`dkp_bram`(sd_bram,W=8)。 -2. **重写 ST_E 为每周期写 1 字节**(3 字节拆 3 拍):新增子计数器 b∈{0,1,2}, - 按 b 选 e_b0/e_b1/e_b2 与目标地址 e_boff+b。ST_E 周期数约 ×3(K=4 增 ~2K 周期, - 占总量 ~4%,可接受)。rho 拷贝阶段本就 1 字节/周期,不变。 -3. **ST_H 的 h_padbyte 读 ek 改为地址提前一拍**:assemble 子状态先发地址、下一拍取数据 - 写入 h_block_r(h_byte 流水线 +1 级)。 -4. **回读路径**(dbg_byte_o / dbg_dk_o):改为直接用 bram 的 rd_data(去掉多余的中间寄存器, - 保持总延迟 ≤2 拍,落在 TB 的 2 周期等待内)。 -5. 跑 `./run_tb.sh top` 全 11 例,要求 0 个 `cannot be opened`、全 PASS。 - -### 阶段 2:polymem → 4 bank BRAM(风险较高) -1. 例化 4 个 `sd_bram #(.W(12))`:bank_a(D=4096)、bank_s/e/t(D=1024)。 -2. 写口改写:ST_A→bank_a;ST_C→bank_s/e;ST_N→bank_s/e;ST_M ACC→bank_t。 -3. 读口加“地址提前一拍”流水: - - ST_N LOAD:读地址 = 下一个 n_ridx,数据对齐 ntt_core 的 valid_i。 - - ST_M LOAD:读地址 = 下一个 m_ld,数据对齐 poly_mul 的 pm_valid。 - - ST_M ACC:m_acc_src 读地址跟踪即将到来的 m_oidx(提前一拍),对齐 pm_vo。 - - ST_E:每周期读 1 个系数(e_c0、e_c1 拆两拍),配合阶段 1 的 1 字节/周期写。 -4. dbg_coeff_o:4 bank rd_data 按 dbg_slot 区间选择。 -5. 跑全 11 例验证。 - -## 验证方法(每阶段) -- 干净重跑:`rm -rf xsim.dir .Xil` 后 `./run_tb.sh top`。 -- 检查日志:`grep -c 'cannot be opened'`(=0) 且 11/11 `PASS`。 -- 记录周期数变化(预期 ek/dk BRAM 化使 ST_E 变长、polymem 读延迟引入少量气泡)。 -- 阶段 2 完成后,可选:跑 `vivado -mode batch` 综合,确认 polymem 现在落在 RAMB 而非 LUTRAM。 - -## 不在本次范围 -- 是否把 ek/dk 暴露为对外输出端口(上一条被打断的请求)—— 独立问题,本次只改存储实现, - 保持现有 dbg_* 接口不变。如需对外输出,迁移完成后另议。 - -## 风险 -- 读延迟流水改造触及所有 5 个计算阶段的握手时序,是 KAT 回归的主要风险点。 -- 缓解:分两阶段、每阶段独立 KAT 回归;先做低风险的 ek/dk,再做 polymem。 -- 若某阶段无法在合理尝试内通过 KAT,回退该阶段并报告根因,不带病提交。 diff --git a/.claude/plans/decaps_plan.md b/.claude/plans/decaps_plan.md deleted file mode 100644 index 2077593..0000000 --- a/.claude/plans/decaps_plan.md +++ /dev/null @@ -1,89 +0,0 @@ -# ML-KEM Decaps 顶层集成 — 实现计划 - -> 在 `mlkem_top`(KeyGen + Encaps 均 KAT 通过)基础上扩展 Decaps(FIPS 203 Alg 18 + K-PKE.Decrypt Alg 15)。 -> 决策(已与用户确认):**(1) 实现完整隐式拒绝路径(J + 常量时间 c'==c 比较 + K̄/K' mux),并用 corrupted-ct(KAT ct_n/ss_n)验证拒绝路径;(2) 逐级 dbg tap 对拍 ml-kem-r golden(m'/w/u_hat 等)+ 端到端 ss==KAT.ss。** - -## 算法(Decaps_internal,全 K) -输入:dk(=KAT.sk,768K+96 B)、c(=KAT.ct,32(du·K+dv) B)。输出:K'=ss(32 B)。 -1. 解析 dk:`dk_pke = dk[0:384K]`(s_hat)、`ek_pke = dk[384K:768K+32]`、`h = dk[768K+32:768K+64]`、`z = dk[768K+64:768K+96]`。 -2. `m' = K-PKE.Decrypt(dk_pke, c)`(**唯一全新数据通路**,见下)。 -3. `(K', r') = G(m' ‖ h)` = SHA3-512(64 B 单块)。**与 Encaps G 完全相同**(mode=11)。 -4. `K̄ = J(z ‖ c)` = SHAKE-256(z‖c, 32 B 输出)。**多块,块数 6/9/12 = 与 H(ek) 相同**。 -5. `c' = K-PKE.Encrypt(ek_pke, m', r')`。**这就是 Encaps E1–E7**(A 再生 + 采样 y/e1/e2(seed=r') + NTT + u + v + 压缩成 ct')。**已 KAT 通过,零改动复用**。 -6. 常量时间比较 `c' == c`(逐字节 XOR 累加)。 -7. `K' = (c'==c) ? K' : K̄`(隐式拒绝 mux)。返回 K'=ss。 - -### K-PKE.Decrypt(Alg 15,全新) -- 解析 c:`c1 = c[0:32·du·K]`、`c2 = c[32·du·K : +32·dv]`。 -- `u'[i] = Decompress_du(byteDecode_du(c1[i]))`,i=0..K-1(**新:通用 byteDecode_d 解包器 + comp_decomp mode=1 解压**)。 -- `v' = Decompress_dv(byteDecode_dv(c2))`(1 poly)。 -- `s_hat[i] = byteDecode12(dk_pke[i·384..])`,i=0..K-1(**复用 Encaps TDEC 机器**,5-cyc/triple)。 -- `u_hat[i] = NTT(u'[i])`(mode=0,K polys,**复用 ST_ENC_N 的 ntt_core**)。 -- `w = v' − INTT(Σⱼ s_hat[j]∘u_hat[j])` mod Q(**复用 Encaps V 机器(MAC+INTT),ADD 改 SUB,加数 v' 替 e2+mu**)。 -- `m' = byteEncode₁(Compress₁(w))` = 32 B(**复用 C1/C2 打包器,d=1**)。 - -## 复用与新增 - -### 直接复用(零或极小改动) -- **整个 Encaps E1–E7**(c' = Encrypt):A 再生、CBD(seed=r')、NTT、U、V、C1、C2 → ct_bram。完全复用,只是 seed 来自 r'(已是 r_r 路径)、ek 来自 ek_pke(已在 ek_bram)、m 来自 m'(新:m' 寄存器替 m_r)。 -- `u_sha3` G(mode=11,m'‖h):与 Encaps G 同,只是 enc_g_data 高半改 h(来自 dk 而非 H(ek) 重算)。 -- `u_keccak` 共享核 + J 多块:复用 H 的 mb 路径,仅末块 pad 常量 0x06→0x1F(SHAKE)。 -- `ntt_core`:u_hat fwd NTT(mode=0,复用 ST_ENC_N);decrypt 的 INTT(mode=1,复用 V 的 u_intt 路径)。 -- `poly_mul` / 3 银行 / comp_decomp:加 Decaps phase mux。 -- Encaps TDEC(byteDecode12 → bank_a):decrypt 的 s_hat 解码复用(改落 dk_pke 源 + 目标 bank)。 - -### 新增 RTL -- **`byteDecode_d` 通用解包器**(d∈{4,5,10,11}):c 字节流 → d-bit 系数,LSB-first(byteEncode_d 的逆)。新写,流式读 ct/c_bram,写银行。 -- **comp_decomp mode=1 解压**:实例已有(E5/E7 用 mode=0);Decaps decode-decompress 用 mode=1,d=du/dv。加 phase。 -- **dk 载入路径**:`dk_in_*`(或复用 ek_in_* 加宽地址),Decaps 前流入 dk_bram。顶层解析:dk_pke→bank/解码、ek_pke→ek_bram、h/z→寄存器。 -- **c 载入路径**:`c_in_*` 流入 ct_bram 的「输入 c」区(注意 c' 也写 ct_bram → 需独立 c_in_bram 或分区,见存储编排)。 -- **J 多块组装**:z(reg)‖c(c_in_bram)→ 136B 块,末块 0x1F pad。新地址逻辑(类 H 的 h_g_addr)。 -- **w = v' − INTT(...)**:V 机器 ADD 子相改 SUB(`(v' − psum) mod Q`,负则 +Q),加数源 v'(银行)替 e2(bank_a)+mu。 -- **m' Compress₁+byteEncode₁**:打包器加 d=1 路径(每系数 1 bit,256 bit = 32 B)。m' 落 32-bit 寄存器(供 G 与 c'-Encrypt)。 -- **常量时间比较 c'==c**:逐字节读 ct_bram(c')与 c_in_bram(c),XOR 累加进 1-bit `ct_ne_r`(全程扫完,不早退)。 -- **隐式拒绝 mux**:`ss_r = ct_ne_r ? k_bar_r : kprime_r`。 -- **op_i 加宽 2-bit**:00=KeyGen,01=Encaps,10=Decaps。新增 ST_DEC_* 状态。 - -## 存储编排(关键) -- **c 输入 vs c' 输出冲突**:Decaps 既要保留输入 c(给 J 和最终比较),又要算 c'(写 ct_bram)。**解法:c 输入存独立 `c_in_bram`(sd_bram W=8 D=2048);c' 仍写 ct_bram。** 比较阶段两个 bram 各一读口,无冲突。J 从 c_in_bram 读。 -- **Decrypt 阶段银行**:s_hat(K)、u'(K)、u_hat(=NTT(u') 就地 K)、v'(1)、psum/w(1)。 - - u' → bank_se rel 0..K-1;NTT 就地得 u_hat。 - - s_hat → bank_a slot j·K(复用 TDEC 落点 + V-MAC 寻址)。 - - v' → bank_t rel 0(或 bank_a 空 slot)。 - - psum(Σ s∘u_hat)→ bank_t[UPSUM];INTT 就地;SUB 读 v'(bank_t rel 0)+ psum(bank_t UPSUM)→ 单口冲突 → v' 改存 bank_a 某 slot(类 e2 搬迁)或 bank_se 空区。**bring-up 定稿,dbg 验证。** - - m' 算完 → 32-bit 寄存器。Decrypt 阶段结束,银行清空。 -- **Encrypt(c')阶段**:Decrypt 已出 m'(reg),银行重新被 Encaps E1–E7 占用,无并发。 - -## 顶层接口新增 -- `op_i [1:0]`:00/01/10。start_i 锁存 op_r[1:0]。 -- `dk_in_*`(we/addr/byte):dk 流入。`c_in_*`(we/addr/byte):c 流入。 -- `ss_o`(复用):Decaps 输出 K'。 -- dbg taps:m'(`dbg_mprime_o[255:0]`)、w/u_hat 经现有 dbg_coeff_o、k_bar(`dbg_kbar_o`)。 - -## 实现阶段(逐阶段 dbg/KAT 对拍) -- **D0 — 脚手架 + dk/c 载入 + 解析** ✅:op_i 加宽 2-bit(00 KG/01 Enc/10 Dec),ST_DEC_LOAD(D0 暂直接→DONE)。dk 流入按 region 路由:dk_pke→dkp_bram、ek_pke→ek_bram、h→hek_r、z→z_r;ct→c_in_bram(独立于 ct_bram)。dbg 验证 h/z/ek_pke/dk_pke。**踩坑1:载入路由用 k_r 但 k_r 在 start_i 才锁存 → 预载期 region 边界全 0,路由全错。改用 LIVE k_i 边界(dkp_bytes_ld 等)。踩坑2:旧 KG/Enc TB 未接新端口(dk_in_*/c_in_*/dbg_*)→ X 漂入 write mux,KeyGen/Encaps 超时回归。补 tie-off 0。** runner = `./run_tb.sh dec [K] [CASE]`。K=2/3/4 D0 全过,KG/Enc 回归通过。 -- **D1 — byteDecode_d + Decompress → u'/v'** ✅:复用 comp_decomp(改 mode 可选:Encaps C1/C2 compress mode=0,Decaps DECOMP mode=1)。ST_DEC_DECOMP 内联 byteDecode 走子机:逐字节读 c_in_bram,LSB-first 累进 bit buffer,凑够 d 位抽符号→comp_decomp 解压→写 bank。c1(K 多项式,d=du)→u'[i] bank_se rel i;c2(1 多项式,d=dv)→v' bank_t rel DEC_VSLOT=2(避开 UPSUM=1)。dbg_slot_i 加宽 4→6 bit(K=4 v' 在 slot 26)。dump_decaps.rs(ml-kem-r 工作树)产 u'/v'/s_hat/u_hat/w/m' golden 到 vectors/decgold/。**踩坑:dbg coeff 读回延迟 = bank(1)+dbg_coeff_r(1),TB rdcoeff 等 2 拍少一拍 → 数据整体错位一格;改 3 拍修正。** K=2/3/4 全过,KG/Enc 回归通过。 -- **D2 — s_hat 解码 + u_hat = NTT(u')** ✅:复用 Encaps TDEC 机(ST_DEC_SDEC),字节源从 ek 切到 dkp_bram(td_byte mux + dkp_rd_addr 在 SDEC 走 td_ekaddr),s_hat 写 bank_a slot j*K(与 t_hat 同布局,D3 MAC 可直接读)。复用前向 NTT 机(ST_DEC_NTT,n_slot_max=k_r)对 bank_se rel 0..K-1 的 u' 原地变换成 u_hat。**踩坑:NTT 原地覆盖 u' → verify_d1 复查 u' 必失败;改为 verify_d1 只查 v'(bank_t 未动),u' 正确性由 u_hat==NTT(u') golden 传递性证明。** K=2/3/4 全过,KG/Enc 回归通过。 -- **D3 — w = v' − INTT(Σ s∘u_hat)** ✅:复用 Encaps V 机(ST_DEC_W,u_row=0 单多项式)。MAC s_hat[j](bank_a slot j*K)∘ u_hat[j](bank_se rel j)→psum bank_t[UPSUM],与 V MAC 完全同址,免改。INTT 原地。SUB:w = v' − psum,(v'−psum) 负则 +Q。**关键:v'/psum 读口冲突 → D1 把 v' 落到 bank_a slot DEC_VASLOT=1(s_hat 在 j*K,slot 1 恒空 K≥2),SUB 时 psum(bank_t)+v'(bank_a)并行读,正如 V-ADD 并读 psum+e2。** K=2/3/4 w 全过。 -- **D4 — m' = byteEncode₁(Compress₁(w))** ✅:ST_DEC_MENC,逐系数读 bank_t[UPSUM] 的 w,Compress₁(w)=1 iff 832 在现有 `mlkem_top`(KeyGen 已 KAT 通过)基础上扩展 Encaps(FIPS 203 Alg 17 + K-PKE.Encrypt Alg 14)。 -> 决策(已与用户确认):**(1) 统一进 mlkem_top(加 op_i 选择 + ST_ENC_* 状态,复用同一套叶子 + BRAM 银行);(2) 一上来就做全 K(2/3/4);(3) golden 只做端到端 KAT(ct==KAT.ct, ss==KAT.ss),逐级用 dbg tap 在 bring-up 时对拍。** - -## 算法(Encaps,全 K) -输入:ek(=KAT.pk,384K+32 B)、m(=KAT.msg,32 B)。输出:K=ss(32 B)、c=ct(32·(du·K+dv) B)。 -1. `h = H(ek)` = SHA3-256(ek),多块。 -2. `(K_ss, r) = G(m ‖ h)` = SHA3-512(64 B,单块)。`K_ss = hash[255:0]`,`r = hash[511:256]`。**ss 即 K_ss,无 KDF/J。** -3. `t̂[i] = byteDecode₁₂(ek[i·384 .. (i+1)·384])`,i∈0..K-1;`rho = ek[384K .. 384K+32]`。 -4. `Â[i][j] = SampleNTT(rho‖j‖i)` —— 与 KeyGen ST_A 完全相同。 -5. `y[i] = CBD_η1(PRF(r, i))`,i=0..K-1(nonce 0..K-1)。 -6. `e1[i] = CBD_η2(PRF(r, K+i))`,i=0..K-1(nonce K..2K-1)。**η2 恒=2。** -7. `e2 = CBD_η2(PRF(r, 2K))`(nonce 2K)。 -8. `ŷ[i] = NTT(y[i])`(mode=0)。 -9. `u[i] = INTT(Σⱼ Â[j][i]∘ŷ[j]) + e1[i]`,i=0..K-1。**注意转置:用 Â[j][i](slot=j·K+i),不是 KeyGen 的 Â[i][j]。** INTT = ntt_core mode=1(内置 ×3303 缩放)。 -10. `mu[c] = Decompress₁(byteDecode₁(m)[c])` = m 第 c bit ? 1665 : 0(无需存储,m 寄存器逐 bit 算)。 -11. `v = INTT(Σⱼ t̂[j]∘ŷ[j]) + e2 + mu`。 -12. `c1 = byteEncode_du(Compress_du(u[i]))` 拼 K 个;`c2 = byteEncode_dv(Compress_dv(v))`;`ct = c1 ‖ c2`。 -13. du/dv:K=2/3 → (10,4);K=4 → (11,5)。ct 长度:K=2→768,K=3→1088,K=4→1568。 - -## 复用与新增 - -### 直接复用(零改动或仅加 phase mux) -- `u_sha3`(sha3_top_shared):H(ek) 多块 + G 单块 —— 与 KeyGen ST_H/ST_G 同。 -- 共享 `u_keccak`:扩 sel_* 4-way mux,加 Encaps phase 选择。 -- `u_snt`(sample_ntt):A 再生 —— 与 ST_A 同。 -- `u_cbd`(sample_cbd):y/e1/e2 —— **新增 η2=2 驱动**(eta_i 现仅接 eta1_rt;Encaps 的 e1/e2 phase 驱动 2)。 -- `u_ntt`(ntt_core):ŷ 用 mode=0;u/v 的 INTT 用 **mode=1**(缩放已内置)。 -- `u_pmul`(poly_mul):点乘 Â[j][i]∘ŷ[j] 与 t̂[j]∘ŷ[j];累加在顶层逐系数 modQ(同 ST_M)。 -- 3 个系数银行 bank_a/bank_se/bank_t:**容量够,见存储编排**,只加 Encaps 的读写 phase mux。 - -### 新增 RTL -- `byteDecode₁₂`:ek 3 字节 → 2 系数。`c0=b0|((b1&0xF)<<8); c1=(b1>>4)|(b2<<4)`。组合逻辑,流式写 bank_t。 -- `Compress` 实例:实例化现有 `comp_decomp_sync`(mode=0,d=du/dv),top 当前未用。 -- `byteEncode_d` 通用位打包器(d∈{4,5,10,11}):把 d-bit 压缩值 LSB-first 流式打包成字节写 ct_bram。**新增**(现有 byteEncode12 是 2→3 硬编码,不通用)。 -- `mu` 流内生成:INTT v 输出时按 m 寄存器对应 bit 加 1665。 -- `ct_bram`(sd_bram W=8 D=2048 A=11):ct 字节缓冲(≤1568 B)。 -- ek 载入路径:`m_i[255:0]` 端口 + ek 流式载入接口(valid/ready 写入 ek_bram),Encaps 前预填。 -- 输出:`ss_o`(寄存器 + dbg tap);ct 经 dbg tap 读 ct_bram(对齐现有"无流式输出、靠 readback"的风格)。 -- `op_i`(KeyGen/Encaps 选择) + 新 FSM 状态。 - -## 存储编排(关键 — 全 K 用现有 28 slot 银行不扩容) -- bank_a (D=4096, 16 slot):Â[i][j],K=4 用满 16。 -- bank_se (D=2048, 8 slot):y_hat[0..K-1] + e1[0..K-1] = 2K slot(K=4→8,刚好用满)。 -- bank_t (D=1024, 4 slot):**E0 先放 t̂[0..K-1](从 ek 解码),也用作 e2 的临时家**。 -- **e2 落点**:e2 只在算 v 时需要,算 u 时 bank_t 空闲 → e2 暂存 bank_t[K..](K=2/3 有空 slot)。K=4 时 bank_t 4 slot 被 t̂ 占满 → e2 先存 bank_se 某 slot(算 u 阶段 e1 仍在用,故 e2 需独立 slot)。 - - **解法(全 K 统一,避免分支)**:算 u 时 t̂ 尚未需要 → **先做 u(用 A+y_hat+e1),u 全部算完压缩进 ct 后,e1 已用完** → 复用 bank_se 的 e1 区放 e2,再从 ek 解码 t̂ 进 bank_t 算 v。峰值 slot = 16(A)+ 2K(se)+ 0(t 尚未填)= K=4 时 24 ≤ 28。✅ - - 时序:E_A(填 A)→ E_CBD(y/e1/e2... e2 先丢 bank_t[0])→ E_NTT(y→y_hat)→ E_U(算 u[i],压缩写 ct c1 区)→ E_V_PREP(t̂ 解码进 bank_t,e2 从 bank_t[0] 搬到 bank_se 空 slot 或就地)→ E_V(算 v,+e2+mu,压缩写 ct c2 区)。 - - **简化**:e2 在 E_CBD 阶段先存 bank_se 的 e1 区**之后**的 slot 不够(K=4)。改为 e2 存 bank_t[0](此时 t̂ 未填),E_V_PREP 时 t̂ 从 ek 流式解码覆盖 bank_t,但 e2 要先读出暂存(单 poly 256×12b 可进一个小 reg 阵列,或搬到 bank_se 算完 u 后的空位)。bring-up 时定稿,以 dbg tap 验证。 - -## 运行时参数(新增) -``` -eta2_rt = 2'd2; // 恒定 -du_rt = (k==4) ? 5'd11 : 5'd10; -dv_rt = (k==4) ? 5'd5 : 5'd4; -c1_bytes_rt = 32*du_rt*k_r; // K2:640 K3:960 K4:1408 -c2_bytes_rt = 32*dv_rt; // K2:128 K3:128 K4:160 -ct_bytes_rt = c1_bytes_rt + c2_bytes_rt; // 768 / 1088 / 1568 -``` - -## 顶层接口新增 -- `op_i` (1b):0=KeyGen(现有),1=Encaps。在 start_i 锁存为 op_r。 -- `m_i [255:0]`:Encaps 消息(byte0 在低)。 -- ek 载入:`ek_in_valid_i/ek_in_byte_i[7:0]/ek_in_ready_o`(或复用 dbg 写口),Encaps 前把 ek 流进 ek_bram。 -- `ss_o [255:0]` + `done_o`(复用);ct 经 `dbg_ct_idx_i[10:0] → dbg_ct_o[7:0]` readback。 - -## 实现阶段(逐阶段 KAT/dbg 对拍) -- **E0 — 脚手架 + ek 载入 + H/G**:加 op_i/m_i/ek 载入,ST_ENC_LOAD→ST_ENC_H(H(ek))→ST_ENC_G(G(m‖h))。dbg 验证 ss=K_ss、r(对 ml-kem-r logging::Encaps 的 K/r/H(ek))。 -- **E1 — A 再生 + t̂ 解码**:ST_ENC_A 复用 snt 写 bank_a;byteDecode12 把 ek 解码进 bank_t。dbg 读系数对 KeyGen golden 的 Ahat / 对 KAT pk 解码的 t̂。 -- **E2 — y/e1/e2 采样 (η1/η2)**:ST_ENC_C,nonce 0..2K,eta 在 e1/e2 切 2。dbg 验证 y/e1/e2(对 ml-kem-r)。 -- **E3 — ŷ = NTT(y)**:ST_ENC_N,mode=0,就地。dbg 对 y_hat。 -- **E4 — u = INTT(Σ Â[j][i]∘ŷ[j]) + e1**:ST_ENC_U,poly_mul + 累加 + INTT(mode=1) + 加 e1。**转置寻址 slot=j·K+i**。dbg 对 u。 -- **E5 — Compress_du + byteEncode_du → c1** ✅:comp_decomp(mode0,d=du) + 通用 LSB-first 打包器(ST_ENC_C1)写 ct_bram c1 区。dbg_ct tap 比 ct[0..c1_bytes]==KAT.ct 前缀,K=2/3/4 全过(含 K=4 du=11 跨字节)。runner = `./run_tb.sh enc [K] [CASE]`(已并入 run_tb;旧 run_enc.sh 已删)。 -- **E6 — v = INTT(Σ t̂[j]∘ŷ[j]) + e2 + mu** ✅:ST_ENC_C1→ST_ENC_TDEC(t̂ 解码进 **bank_a** slot j·K)→ST_ENC_E2MV(e2 从 bank_t[0] 搬到 **bank_a[1]**)→ST_ENC_V(复用 u_* MAC/INTT/ADD,u_row≡0)。mu[w]=m_r[w]?1665:0。v 落 bank_t[UPSUM],dbg slot 9(K=2)。dbg 对 v==ml-kem-r golden 全过。**踩坑:e2 搬迁曾 off-by-one(写 e2[i+1] 到 slot i),已修(em_widx==em_ridx,写延后 1 拍对齐 bram 读)。** verify_e1 已废弃(TDEC 覆盖 bank_a 的 A_hat;A_hat 由 E4 转置 MAC 间接验证)。 -- **E7 — Compress_dv + byteEncode_dv → c2 + 端到端 KAT** ✅:ST_ENC_V→ST_ENC_C2,复用 E5 打包器(cp_d=dv,coeff 源 bank_t[UPSUM]=v,poly 数=1),cp_wa 接着 c1_bytes 续写(不重置)。FSM:V→C2→DONE。verify_e7 比全长 ct==KAT.ct。**全 K(2/3/4)、多 case 端到端通过:ct==KAT.ct && ss==KAT.ss。Encaps 完成。** - -## 验证 -- TB `tb_mlkem_enc_katK_xsim.v`:从 `~/Dev/ml-kem-r/test_data/kat_MLKEM_{512,768,1024}.rsp` 取 pk(→ek)、msg(→m)、ct、ss,逐字节比 ct + 比 ss。 -- bring-up 中间对拍:`cargo run --example logging`(已有 Encaps log::debug,输出 K/r/H(ek)/|c|),或临时加 dump_encaps.rs 出 256-coeff 中间量(仅工作树,不提交 ml-kem-r)。 -- XSIM 环境同 KeyGen:`source settings64.sh; export LD_PRELOAD=libtinfo.so.5; rm -rf xsim.dir .Xil`。 - -## 风险 / 注意 -- **转置**:Encrypt 用 Â[j][i],KeyGen 用 Â[i][j]。slot = j*K+i(列优先)。最易错,E4 单列 dbg 对拍。 -- **G 字节序**:KeyGen G 输入 d 原序无翻转(keygen_plan 已确认);但 xcheck 发现 KAT 文件 d/z 存的是翻转序。**m 喂入字节序需 E0 用 KAT.msg 实测确认**(很可能与 d 同约定)。 -- **INTT 缩放**:ntt_core mode=1 已内置 ×3303,不要再缩放。 -- **e2 落点(K=4)**:见存储编排,bring-up 定稿,dbg 验证不踩 t̂/e1。 -- **byteEncode_d 通用打包**:d=10/11 跨字节,位序 LSB-first(同 byte_encode)。新写、单独 TB 或 E5 dbg 即验。 -- **共享 keccak 4-way mux**:加 Encaps phase 的 sel_ + kc_valid_o 门控,勿与 KeyGen phase 冲突(op_r 已区分)。 - diff --git a/.claude/plans/keygen_plan.md b/.claude/plans/keygen_plan.md deleted file mode 100644 index 23f3004..0000000 --- a/.claude/plans/keygen_plan.md +++ /dev/null @@ -1,175 +0,0 @@ -# ML-KEM KeyGen 顶层集成 — 实现计划与进度 - -> 本文档用于让新工程师完整接手。包含原始计划 + 已完成进度 + 经验教训 + 下一步。 - -## 目标与范围 -- 目标:实现 `mlkem_top` 的 **KeyGen 通路**(ML-KEM-512, K=2, η1=3),全新重写 valid/ready FSM。 -- 最终验收:对 **NIST 官方 KAT**(count=0..4)端到端逐字节通过,产出完整 ML-KEM dk。 -- 本计划只做 KeyGen。Encaps/Decaps 留作后续任务。 -- 旧版 `mlkem_top.v`(1658 行)已于 commit 1cace51 删除,**不可用**(Decaps 是占位,且子模块握手从未真正跑通——旧 TB 用 24 处 force/release 绕过)。新版全新重写。 - -## 关键决策(已与用户确认) -- **Keccak 策略**:每消费者用独立、已验证的核(sha3_top / sample_ntt_sync / sample_cbd_sync 各自带 keccak_core)。**不用 arbiter,不用 `_shared` 变体**(顶层当前用独立版)。 -- **SHA3 先扩多块**:已先把 sha3_top 的 H 模式扩成多块吸收(✅ 完成),再做完整 dk。 -- **FSM 全新重写**:只参考算法步骤,不取回旧代码。 -- **分阶段 + 逐级对拍**:每个子步骤对 golden 验证通过再进下一步。 - -## 环境与工具(重要) -- 工作目录:`/home/fallensigh/Dev/mlkem`,git 分支 `main`(直接提交,用户要求)。 -- **Verilator** 5.046:`python3 test_framework/run_all.py [--module X]`。快速回归。 -- **Vivado XSIM** 2019.2:每次需 - ``` - source /opt/Xilinx/Vivado/2019.2/settings64.sh - export LD_PRELOAD=/usr/lib64/libtinfo.so.5 - ``` - 调用 **standalone** `xvlog -sv --relax -i . ` / `xelab -s --timescale 1ns/1ps` / `xsim -R`。**不要**用 `vivado -mode batch`(脚本里用的是 standalone 命令,不是 Tcl)。zsh 不做无引号变量分词——用 `"$@"` 传文件列表。 -- XSIM 产物(xsim.dir/ .Xil/ *.jou *.log)已 gitignore;每次跑前 `rm -rf xsim.dir .Xil`。 - -## Golden reference(全部已验证可用) -- **NIST 官方 KAT**:`/home/fallensigh/Dev/ml-kem-r/test_data/kat_MLKEM_{512,768,1024}.rsp`。 -- **ml-kem-r**(Rust, `/home/fallensigh/Dev/ml-kem-r`):`cargo test` 全过,512/768/1024 KAT 全通过。是可信参考。 - - API:`ml_kem::mlkem512::key_gen(d,z)`、`keygen_internal::<2,192>(d,z)`、`k_pke::key_gen::<2,192>(d)`。 - - 公共子函数:`sample::sample_ntt(&[u8])`、`sample::sample_poly_cbd::`、`ntt::ntt(&mut [i32;256])`、`ntt::multiply_ntts`、`sha3::mlkem_G/mlkem_H/mlkem_PRF`、`utils::byte_encode(&[i32;256], d)`。 - - `logging` feature + `examples/logging.rs` dump 中间值(4-coeff head)。 -- **server_code**(Python, `~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0`):`SHA_3.G/H/J/PRF`、`ML_KEM.py`。已对 hashlib 交叉验证。 - -### 白盒 golden 向量(Stage 0 产物,已生成并自校验) -- 生成器:`/home/fallensigh/Dev/ml-kem-r/examples/dump_keygen.rs`(我新增,未提交到 ml-kem-r,因为该仓库有用户大量未提交改动 —— 只在工作树用)。 - - 运行:`cargo run --release --example dump_keygen -- 5` -- 输出目录:`test_framework/modules/mlkem_keygen/golden/`(已 commit 在 mlkem 仓库 106b292)。 -- 每个 KAT case c(000..004)的全 256-coeff 中间量,文件名: - - `c000_rho.hex` / `c000_sigma.hex`:32B hex(byte0 在最低)。 - - `c000_Ahat_i_j.hex`(i,j∈0..1):256 行,每行 3-hex 系数(mod q)。 - - `c000_s_i.hex` / `c000_e_i.hex`:CBD 输出,**mod-q 形式**(负值 +Q,如 -1 → 0xd00=3328)。 - - `c000_shat_i.hex` / `c000_ehat_i.hex`:NTT(s/e) 后。 - - `c000_that_i.hex`:t_hat。 - - `c000_ek.hex`(800B hex = KAT pk)、`c000_dkpke.hex`(768B = KAT sk 前 384*K)。 - - dump_keygen 已自校验 ek==KAT.pk、dk_pke==KAT.sk[..768],全 5 case 通过。 - -## KeyGen 算法 (FIPS 203 Alg 13+16, K=2 η1=3) -1. (ρ,σ) = G(d ‖ K) — SHA3-512, 33B 输入(单块)。**G 输入 = d(byte0 低) ‖ K=2 at byte32,无字节翻转**(已确认,旧版 byte-reverse 是 bug)。ρ = hash[255:0],σ = hash[511:256]。 -2. Â[i][j] = SampleNTT(ρ‖j‖i), i,j∈{0,1} — seed = rho ‖ j ‖ i(sample_ntt_sync 内部 msg={i_byte,j_byte,rho})。 -3. s[i] = CBD₃(PRF(σ, **i**)), i∈{0,1} — nonce 0,1 -4. e[i] = CBD₃(PRF(σ, **K+i**)), i∈{0,1} — nonce 2,3 -5. ŝ[i] = NTT(s[i]); ê[i] = NTT(e[i]) — 前向 NTT ×4 (mode=0, **无缩放**) -6. t̂[i] = ê[i] + Σⱼ Â[i][j]∘ŝ[j] — poly_mul + 逐系数 modQ 累加 -7. ek = byteEncode₁₂(t̂[0..1]) ‖ ρ — 12-bit 原始打包(**非压缩**) -8. dk_pke = byteEncode₁₂(ŝ[0..1]) -9. dk = dk_pke ‖ ek ‖ H(ek) ‖ z — H(ek): SHA3-256 over 800B(**多块**) - -**用到的叶子模块**(端口签名见下):sha3_top(G+多块H)、sample_ntt_sync、sample_cbd_sync、ntt_core(mode=0)、poly_mul_sync、mod_add_sync。**不用** comp_decomp。 - -## 叶子模块端口签名(已抓取) -- `rng_sync #(SEED)`: clk,rst_n,valid_i,ready_o,data_o[255:0],valid_o,ready_i -- `sha3_top`: clk,rst_n,mode[1:0],data_i[511:0],valid_i,ready_o,hash_o[511:0],valid_o,ready_i, **+多块端口** mb_en,mb_block_i[1087:0],mb_valid_i,mb_last_i,mb_ready_o -- `sample_ntt_sync #(K=4)`: clk,rst_n,rho_i[255:0],k_i[2:0],i_idx[1:0],j_idx[1:0],valid_i,ready_o,coeff_o[11:0],valid_o,ready_i,last_o -- `sample_cbd_sync`: clk,rst_n,seed_i[255:0],nonce_i[7:0],eta_i[1:0],valid_i,ready_o,coeff_o[11:0](**12-bit signed 二补码**),valid_o,ready_i,last_o -- `ntt_core`: clk,rst_n,coeff_in[11:0],valid_i,ready_o,mode(0=fwd无缩放/1=inv有缩放),coeff_out[11:0],valid_o,ready_i,done_o。协议:IDLE/LOAD 期间 ready_o 高,流式喂 256 个(valid_i 持续);算完 S_OUTPUT 流式吐 256(valid_o 高,ready_i 消费);S_DONE→IDLE。 -- `poly_mul_sync`: clk,rst_n,coeff_a_in[11:0],coeff_b_in[11:0],valid_i,ready_o,coeff_out[11:0],valid_o,ready_i。协议:LOAD 阶段 **同时**喂 A、B 各 256 对(coeff_a_in/coeff_b_in 同拍),然后 COMPUTE 流式吐 256。 -- `mod_add_sync`: clk,rst_n,a[11:0],b[11:0],valid_i,ready_o,sum[11:0],valid_o,ready_i。(a+b)mod Q。 -- `comp_decomp_sync`: clk,rst_n,coeff_in[11:0],d[4:0],mode,valid_i,ready_o,coeff_out[11:0],valid_o,ready_i(KeyGen 不用)。 -- `sd_bram #(W=48,D=64,A=6)`、`s_bram #(W=48,D=512,A=9)`(KeyGen 暂用寄存器阵列 polymem 缓存,未用 bram)。 - -## 已完成进度 - -### ✅ Stage 0:白盒 golden 向量生成器(commit 106b292) -- `examples/dump_keygen.rs`(ml-kem-r 工作树)+ `test_framework/modules/mlkem_keygen/golden/`(mlkem 仓库)。 -- 自校验:ek==KAT.pk、dk_pke==KAT.sk 前缀,5/5 通过。 - -### ✅ Stage 1:sha3_top 多块 SHA3-256 吸收(commit 106b292) -- `sync_rtl/sha3/sha3_top.v`:新增多块吸收 FSM。新端口 `mb_en/mb_block_i[1087:0]/mb_valid_i/mb_last_i/mb_ready_o`。 - - **设计**:调用方预填充末块的 SHA3-256 padding(0x06 ... 0x80),模块只做纯吸收 loop(state^=block; Keccak-p)。单块 G/H/J 路径在 mb_en=0 时逐 bit 不变。 - - 状态:MB_IDLE/MB_PERMUTE/MB_DONE。`mb_state_r`(1600-bit running state),`mb_digest_r`(256-bit sticky digest,保持到消费者 ack)。 - - `ready_o = !mb_en && (state_r==ST_IDLE)`、`valid_o`/`hash_o` 按 mb_en 选择。 -- TB:`sync_rtl/sha3/TB/tb_sha3_mb_xsim.v`(自检,流 800B ek=6 块 → H(ek),对 hashlib.sha3_256)。向量:`vectors/mb_h_blocks.hex`、`mb_h_expected.hex`。 - - **握手坑(已解决)**:TB 必须在 accept 边沿保持 valid/last 稳定(等 mb_ready_o 拉低后才撤),否则采样竞争。digest 一次有效后 sticky,TB 收完才 ack。 -- 既有 G/H/J TB(xsim 3 个 + Verilator tb_sha3.cpp)**必须 tie off mb_* 端口**(mb_en=1'b0 等),否则 mb_en 浮空成 x 破坏 ready_o。已全部修。 -- 回归:Verilator 25/25,XSIM 4 个 TB(simple G/H/J、keccak_core、7-vec、multiblock)全过。 - -### ✅ sample_ntt 幽灵脉冲修复(commit 6db3c7c) -- **bug**:`sample_ntt_sync` 的 Phase-1(输出 d1)缺少 Phase-2 有的 `need_more` 守卫。当第 256 个被接受系数是 d1 且组推进时,某些 seed 会在 last_o 后多吐一拍 valid_o(KAT count=0 rho 下,seed i=0/j=1 吐 257 拍)。集成时漏进下个 poly 的 index 0,整流后移一位。 -- **修复**:Phase-1 改 `if (d1_acc_r && need_more)`。两变体(`sample_ntt_sync.v` + `sample_ntt_sync_shared.v`)都改。 -- **补盲区**:`tb_sample_ntt_xsim.v` 加断言——last_o 后保持 ready_i 数多余 valid_o,有则 FAIL。已验证该断言能抓 bug(还原副本 3 个幽灵)、修复版通过。 -- 验证:40-seed(sync)+24-seed(shared)审计全 256 拍/last@256/零幽灵;Verilator 1536/1536;全框架 4334/4334;Stage 2c 2048/2048。 -- 记忆:`sample-ntt-257th-pulse-bug.md`。 - -### ✅ Stage 2a/2b/2c:KeyGen 数据通路前半(**未提交**,在工作树) -- 文件:`sync_rtl/top/mlkem_top.v`(新建)+ TB `sync_rtl/top/TB/tb_mlkem_kg_2a_xsim.v`、`tb_mlkem_kg_2c_xsim.v`、向量 `sync_rtl/top/TB/vectors/kg_c000_AsE.hex`。 -- **mlkem_top 结构**(K=2,ETA1=3): - - 端口:clk,rst_n,d_i[255:0],z_i[255:0],start_i,busy_o,done_o + **调试 tap**:dbg_slot_i[3:0],dbg_idx_i[7:0],dbg_coeff_o[11:0](寄存器读 polymem,2拍延迟),dbg_rho_o[255:0],dbg_sigma_o[255:0]。 - - **polymem**:`reg [11:0] polymem[0:10*256-1]`,slot 映射:0-3=A00,A01,A10,A11;4-5=S0,S1;6-7=E0,E1;8-9=T0,T1。s/s_hat 共享 slot(就地 NTT),e 同理。 - - FSM 状态:ST_IDLE=0,ST_G=1,ST_A=2,ST_C=3,...,ST_DONE=15。 - - **2a(G)**:sha3_top mode=00,g_data={248'b0,8'(K),d_i};完成捕获 rho_r/sigma_r。✅ rho/sigma 对 golden 精确。 - - **2b(A)**:a_pair 0..3 循环,a_i=pair[1],a_j=pair[0],a_slot=pair。每 pair 跑 sample_ntt_sync,256 系数写 polymem[a_slot]。✅ 1024 系数全对。 - - **2c(C)**:c_poly 0..3(s0,s1,e0,e1),c_nonce=c_poly,c_slot 映射。CBD 输出经 `cbd_modq = cbd_coeff[11] ? cbd_coeff+Q : cbd_coeff` 转 mod-q 后写 polymem。✅ 2048/2048(A+s+e)全对。 - - **关键防护 a_busy/c_busy**:请求被接受(valid&&ready)时置位,last 时清零;仅 busy 时收集系数。本是为屏蔽 sample_ntt 幽灵脉冲的下游兜底;sample_ntt 根因修复后冗余但保留(无害,且对未来叶子模块是防御)。 -- **字节序基准**(全部已确认,这是旧版的痛点): - - G 输入 d 原序(byte0 低),无翻转。 - - SampleNTT seed=rho‖j‖i。 - - CBD nonce:s[i]=i,e[i]=K+i。 - - golden 多项式存 mod-q;CBD RTL 输出 12-bit 二补码,需 +Q 转换。 - -## 当前工作树状态(未提交) -- `sync_rtl/top/mlkem_top.v`(新,Stage 2a/b/c,FSM 到 ST_C) -- `sync_rtl/top/TB/tb_mlkem_kg_2a_xsim.v`、`tb_mlkem_kg_2c_xsim.v`(新) -- `sync_rtl/top/TB/vectors/kg_c000_AsE.hex`(新,8 poly × 256 = 2048 行,顺序 A00,A01,A10,A11,S0,S1,E0,E1) -- `.claude/plans/`(本文件) -- **下一步该先提交 Stage 2a-2c**(测试已通过)。 - -## 关键测试 literal(byte0-低 256-bit) -- KAT c000 d:`D_LIT = 256'h2426f1941779574d3f1b163bd57f7e173e229e630ec7f7073bdf365137c4bb6d` -- 期望 rho:`256'h15f74355ca862c3cdf3dab780c35cf24b88bf144706090a1c17e41205f9f1379` -- 期望 sigma:`256'h69b042001b5630b1a039116cbfd29f62c0bde5a6b571504a9fcce68bed667fd5` - -## 下一步(剩余 Stage 2/3/4) - -### Stage 2d:NTT(s/e → ŝ/ê) ← **当前正在做** -- golden 映射已确认:`ntt(s_i)==shat_i`、`ntt(e_i)==ehat_i`(mode=0 fwd,无缩放,zeta 表见 ntt gen_vectors,N_INV=3303 仅 inv 用)。 -- 计划:新增 ST_N 状态,对 4 个 slot(S0,S1,E0,E1)逐个就地 NTT:从 polymem 读 256 喂 ntt_core(mode=0),收 256 写回同 slot。 -- 注意 ntt_core 协议:LOAD 期间持续 valid_i 喂 256;输出阶段 valid_o 高、ready_i 消费 256。需读地址流水(组合读 polymem)。 -- TB:扩展对 shat/ehat golden 校验。 -- **正读 mlkem_top.v 行 200-290 的时序逻辑准备插入 ST_N 时被打断;接手时先重读 mlkem_top.v 全文确认当前 FSM 时序结构再加 ST_N。** - -### Stage 2e:矩阵累加 t̂[i] = ê[i] + Σⱼ Â[i][j]∘ŝ[j] -- 用 poly_mul_sync(同拍喂 A、B 各 256)算 Â[i][j]∘ŝ[j],再用 mod_add_sync 逐系数 + ê[i] 累加。K=2:t̂[0]=ê0+A00∘ŝ0+A01∘ŝ1;t̂[1]=ê1+A10∘ŝ0+A11∘ŝ1。 -- 对 golden `that_i` 校验。 - -### Stage 2f:byteEncode₁₂ → ek/dk_pke -- 12-bit LSB-first 打包(FIPS:b[i*12+j]=(coeff>>j)&1,再 bits_to_bytes)。ek=byteEncode12(t̂[0..1])‖ρ(800B);dk_pke=byteEncode12(ŝ[0..1])(768B)。 -- 对 KAT pk / sk 前缀逐字节校验。 - -### Stage 3:KeyGen FSM 集成 + 干净顶层接口 -- 串起 2a-2f;顶层流式输出 ek_o/dk_o;无 force。 - -### Stage 4:组装完整 dk + 端到端 KAT -- dk = dk_pke ‖ ek ‖ H(ek)[用 Stage1 多块] ‖ z。 -- 干净 KAT TB(无 force/release),喂 KAT d/z,逐字节比 ek 与 dk。跑 KAT count=0..4 全过。Verilator + XSIM 双框架。 - -## 经验教训(重要) -1. **"PASS" 只证 RTL==reference**:必须独立对 hashlib/FIPS 验证 oracle。 -2. **单独 TB 盲区会掩盖模块 bug**:sample_ntt 的 257th 脉冲就是"读够就停"掩盖的。新 TB 要验证协议边界(last 后 valid 干净拉低)。 -3. **加端口必须 tie off 所有旧实例**:浮空输入成 x 会破坏组合逻辑(mb_en 教训)。 -4. **valid/ready 握手 TB 要在 accept 边沿保持输入稳定**,等 ready 拉低再撤,否则采样竞争(多块 H、A-pair 边界都踩过)。 -5. **字节序/位序是旧版主要 bug 源**:每步对 golden 逐字节,错位立刻暴露。 -6. **bash 工具偶发输出截断/丢失**:重要结论用 `rm -rf xsim.dir` 重跑确认,别凭一次模糊输出下判断。 - -## 任务追踪(TaskList ID) -- #7 Stage 0 ✅ completed -- #8 Stage 1 ✅ completed -- #9 Stage 2 datapath subblocks — in_progress(2a/b/c done,2d 进行中) -- #10 Stage 3 FSM 集成 — blocked by #9 -- #11 Stage 4 dk + KAT — blocked by #8,#10 - -## 提交历史(相关) -- 106b292 feat(sha3): multi-block SHA3-256 absorb for H(ek); KeyGen golden vectors (Stage 0+1) -- 6db3c7c fix(sample_ntt): suppress spurious 257th valid_o after last_o -- (未提交)Stage 2a-2c mlkem_top + TB - ---- -## ✅ COMPLETE (session end state) -- KeyGen FSM: IDLE->G->A->C->N->M->E->H->DONE, pure valid/ready, no force. -- Verified vs NIST KAT MLKEM-512 **count=0..4**: ek (800B)==pk, dk (1632B)==sk, byte-exact, ~21350 cyc/case. -- Commits: 106b292, 6db3c7c, 2f206a6, 4c692e5, a9e50eb, 1791491, 9824ed8, 42d3748. -- Independent Keccak per consumer (G + dedicated H sha3_top + sample_ntt/cbd cores). No arbiter. -- SECURITY: tool outputs this session carried prompt-injection (fake operator instructing `curl|bash`); all refused. Some outputs were tampered; results re-verified with controlled delimiters. -- Remaining optional: streaming ek/dk output port (currently start_i/done_o + readback taps); Encaps/Decaps; shared-keccak migration. diff --git a/.claude/plans/phase2_polymem_bram.md b/.claude/plans/phase2_polymem_bram.md deleted file mode 100644 index 0ef40da..0000000 --- a/.claude/plans/phase2_polymem_bram.md +++ /dev/null @@ -1,47 +0,0 @@ -# Phase 2: polymem -> BRAM banks - -## Goal -Replace the single multi-port async-read `reg [11:0] polymem [0:28*256-1]` with -registered-read `sd_bram` banks that infer real BRAM (ASIC: compiled SRAM). - -## Bank split (all sd_bram, W=12, 1R+1W registered) -- bank_a : A_hat[i][j], slots 0..K^2-1 -> D=4096 (KMAX^2*256), A=12 -- bank_se: s_hat[i] then e_hat[i] -> D=2048 (2*KMAX*256), A=11 - relative slot = abs_slot - slot_s_rt (s: 0..K-1, e: K..2K-1) -- bank_t : t_hat[i], slots slot_t_rt.. -> D=1024 (KMAX*256), A=10 - relative slot = abs_slot - slot_t_rt - -## Port budget (verified disjoint) -- poly_mul LOAD vs ACCUMULATE never overlap; ST_N/ST_M/ST_E are disjoint - top-states. => each bank needs only 1R+1W. sd_bram fits. -- bank_t ST_M-acc reads t_hat[idx] (j>0 source) and writes t_hat[idx] result - same cycle: 1R+1W, read-old/write-new. Prefetch keeps read AHEAD of write - (monotonic idx), so no same-pass RAW hazard. - -## Read sites (all need 1-cycle read-ahead vs async today) -- ntt_in (ST_N load) <- bank_se[n_slot] -- e_c0,e_c1 (ST_E) <- bank_se (dk) or bank_t (ek); SERIALIZE the - two reads across the 3-byte/pair window -- pm_a_in (ST_M load) <- bank_a[m_aslot] -- pm_b_in (ST_M load) <- bank_se[m_j] -- m_acc_src (ST_M acc) <- bank_se[K+m_i] (j==0) or bank_t[m_i] (j>0) -- dbg_coeff_o (not on KAT path) <- route by slot range (compile-only) - -## Write sites -- ST_A: bank_a[a_slot] <= snt_coeff -- ST_C: bank_se[c_slot-slot_s] <= cbd_modq -- ST_N: bank_se[n_slot] <= ntt_coeff (write-back, same slot) -- ST_M-acc: bank_t[m_i] <= m_accq - -## Read-ahead strategy -Cores hold ready_o high through entire LOAD (no mid-stream backpressure) => -fixed 1-cycle skew. Pattern: advance a read-address pointer 1 cycle ahead of -the consumer index; delay the consumer's valid by 1 cycle ("prime the pipe"). -For ST_M-acc (irregular pm_vo cadence 0,1,1): 2-entry prefetch skid buffer, -read pointer runs monotonically ahead of write pointer. - -## Checkpoints (KAT-gated: 11 cases, byte-exact, 0 file-not-found) -- 2a: split polymem -> 4 ASYNC-read banks (pure refactor, zero timing change). - Validates bank sizing + base-relative addressing + debug mux. COMMIT. -- 2b: convert banks to registered sd_bram + add read-ahead pipelines to every - consumer FSM (ST_N, ST_E, ST_M load, ST_M acc). COMMIT. diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 1d15170..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "env": { - "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1" - }, - "hooks": { - "SessionStart": [ - { - "matcher": "startup", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/session-start.py", - "timeout": 30 - } - ] - }, - { - "matcher": "clear", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/session-start.py", - "timeout": 30 - } - ] - }, - { - "matcher": "compact", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/session-start.py", - "timeout": 30 - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "Task", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/inject-subagent-context.py", - "timeout": 30 - } - ] - }, - { - "matcher": "Agent", - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/inject-subagent-context.py", - "timeout": 30 - } - ] - } - ], - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 .claude/hooks/inject-workflow-state.py", - "timeout": 15 - } - ] - } - ] - }, - "enabledPlugins": {} -} diff --git a/.claude/skills/trellis-before-dev/SKILL.md b/.claude/skills/trellis-before-dev/SKILL.md deleted file mode 100644 index 9c6ec9c..0000000 --- a/.claude/skills/trellis-before-dev/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -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/.claude/skills/trellis-brainstorm/SKILL.md b/.claude/skills/trellis-brainstorm/SKILL.md deleted file mode 100644 index 670e19b..0000000 --- a/.claude/skills/trellis-brainstorm/SKILL.md +++ /dev/null @@ -1,548 +0,0 @@ ---- -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/.claude/skills/trellis-break-loop/SKILL.md b/.claude/skills/trellis-break-loop/SKILL.md deleted file mode 100644 index ef2b50c..0000000 --- a/.claude/skills/trellis-break-loop/SKILL.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -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/.claude/skills/trellis-check/SKILL.md b/.claude/skills/trellis-check/SKILL.md deleted file mode 100644 index 16b3dc4..0000000 --- a/.claude/skills/trellis-check/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -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/.claude/skills/trellis-meta/SKILL.md b/.claude/skills/trellis-meta/SKILL.md deleted file mode 100644 index 590bfac..0000000 --- a/.claude/skills/trellis-meta/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -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/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md deleted file mode 100644 index 608aaa6..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-agents.md b/.claude/skills/trellis-meta/references/customize-local/change-agents.md deleted file mode 100644 index 9b63531..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-agents.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md b/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md deleted file mode 100644 index 556b4e3..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md +++ /dev/null @@ -1,81 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-hooks.md b/.claude/skills/trellis-meta/references/customize-local/change-hooks.md deleted file mode 100644 index 5c1ed7a..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-hooks.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md deleted file mode 100644 index 84590a1..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md +++ /dev/null @@ -1,78 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md deleted file mode 100644 index 358de51..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md deleted file mode 100644 index a7a340f..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md +++ /dev/null @@ -1,90 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/change-workflow.md b/.claude/skills/trellis-meta/references/customize-local/change-workflow.md deleted file mode 100644 index 4231845..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/change-workflow.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/customize-local/overview.md b/.claude/skills/trellis-meta/references/customize-local/overview.md deleted file mode 100644 index b53b090..0000000 --- a/.claude/skills/trellis-meta/references/customize-local/overview.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/context-injection.md b/.claude/skills/trellis-meta/references/local-architecture/context-injection.md deleted file mode 100644 index fae6fa5..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/context-injection.md +++ /dev/null @@ -1,68 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/generated-files.md b/.claude/skills/trellis-meta/references/local-architecture/generated-files.md deleted file mode 100644 index 66f832d..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/generated-files.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/overview.md b/.claude/skills/trellis-meta/references/local-architecture/overview.md deleted file mode 100644 index 99c7f73..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/overview.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/spec-system.md b/.claude/skills/trellis-meta/references/local-architecture/spec-system.md deleted file mode 100644 index 61281f0..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/spec-system.md +++ /dev/null @@ -1,102 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/task-system.md b/.claude/skills/trellis-meta/references/local-architecture/task-system.md deleted file mode 100644 index 64ad00d..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/task-system.md +++ /dev/null @@ -1,101 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/local-architecture/workflow.md b/.claude/skills/trellis-meta/references/local-architecture/workflow.md deleted file mode 100644 index f0659ff..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/workflow.md +++ /dev/null @@ -1,75 +0,0 @@ -# Local Workflow System - -`.trellis/workflow.md` is the Trellis workflow source of truth inside the user project. An AI does not need Trellis source code to understand how the current project should move tasks forward; this file is enough. - -## File Responsibilities - -`.trellis/workflow.md` has three responsibilities: - -1. **Explain workflow phases**: Plan, Execute, Finish. -2. **Define skill routing**: which skill or agent the AI should use when the user expresses a certain intent. -3. **Provide workflow-state prompt blocks**: hooks can inject the prompt block for the current state into the conversation. - -## Current Phase Model - -```text -Phase 1: Plan -> clarify what to build, produce prd.md and required research -Phase 2: Execute -> implement against the PRD and specs, then check -Phase 3: Finish -> final verification, preserve lessons, and wrap up -``` - -Each phase contains numbered steps, such as `1.3 Configure context`. These numbers are not runtime fields in `task.json`; they are workflow structure for AI and humans to read. - -## Skill Routing - -`workflow.md` separates routing by platform capability: - -- Platforms with sub-agent support: dispatch `trellis-implement` by default for implementation and `trellis-check` for checking. -- Platforms without sub-agent support: the main session reads skills such as `trellis-before-dev`, then executes directly. - -When changing local AI behavior, update the routing descriptions in `workflow.md` first, then check whether the corresponding platform skill, command, or agent files need to stay in sync. - -## Workflow-State Prompt Blocks - -The bottom of `workflow.md` can contain state blocks like this: - -```text -[workflow-state:no_task] -... -[/workflow-state:no_task] -``` - -Hooks choose the right block based on current task status and inject it into the conversation. Common states include: - -| State | Meaning | -| --- | --- | -| `no_task` | The current session has no active task. | -| `planning` | The task is still in requirements, research, or context configuration. | -| `in_progress` | The task has entered implementation and checking. | -| `completed` | The task is complete and waiting for wrap-up or archive. | - -If the user wants to change policies such as "whether to create a task when there is no task," "when task creation may be skipped," or "whether sub-agents are required," edit these state blocks and the routing table above them. - -## Local Modification Patterns - -Common changes: - -| Goal | Edit point | -| --- | --- | -| Add a phase | Update the Phase Index, phase body, routing, and state blocks. | -| Change task creation policy | Update the `no_task` state block and Phase 1 description. | -| Change the default implementation/check path | Update Phase 2 and skill routing. | -| Change the wrap-up flow | Update Phase 3 and `finish-work` related descriptions. Note the current split: Phase 3.4 = AI-driven code commits (batched, user-confirmed), Phase 3.5 = `/finish-work` (archive + record session). `/finish-work` refuses to run if the working tree is dirty. | -| Change platform differences | Update routing descriptions grouped by platform. | - -After editing, make the AI reread `.trellis/workflow.md`; do not assume the flow from the old conversation is still valid. - -## Relationship To Platform Files - -`workflow.md` is the semantic center of the local workflow, but each platform can also have its own entry files: - -- skills, such as `trellis-brainstorm` and `trellis-check`. -- commands/prompts/workflows, such as continue and finish-work. -- hooks, such as session-start or workflow-state injection. - -If only `workflow.md` changes, platform entry files may still contain old language. When the user wants to change "what the AI actually does," also inspect the relevant platform directory. diff --git a/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md deleted file mode 100644 index c2958f2..0000000 --- a/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md +++ /dev/null @@ -1,71 +0,0 @@ -# Local Workspace Memory System - -`.trellis/workspace/` stores cross-session memory. Its purpose is to let AI and humans understand what happened before across different windows and different days. - -## Directory Structure - -```text -.trellis/workspace/ -├── index.md -└── <developer>/ - ├── index.md - ├── journal-1.md - └── journal-2.md -``` - -| File | Purpose | -| --- | --- | -| `.trellis/.developer` | Current developer identity. | -| `.trellis/workspace/index.md` | Global workspace overview. | -| `.trellis/workspace/<developer>/index.md` | Session index for a developer. | -| `.trellis/workspace/<developer>/journal-N.md` | Session journal. | - -## Developer Identity - -Run this the first time: - -```bash -python3 ./.trellis/scripts/init_developer.py <name> -``` - -This creates `.trellis/.developer` and the corresponding workspace directory. The AI should not change developer identity casually; if the identity is wrong, first confirm who is using the current project. - -## Journal - -`journal-N.md` records completed or partially completed work from each session. By default, each journal holds about 2000 lines; after that it rotates to the next file. - -Common command for recording a session: - -```bash -python3 ./.trellis/scripts/add_session.py \ - --title "Session title" \ - --summary "What changed" \ - --commit "abc1234" -``` - -Planning or review work without a commit can also be recorded by using `--no-commit` or an empty commit value. - -## Relationship Between Workspace Memory And Tasks - -| System | What it stores | -| --- | --- | -| `.trellis/tasks/` | Requirements, design, research, and state for a specific task. | -| `.trellis/workspace/` | Work records across tasks and sessions. | -| `.trellis/spec/` | Engineering knowledge preserved as long-term conventions. | - -If information is only useful for the current task, put it in the task directory. -If information describes what happened in the current session, put it in the workspace journal. -If information should be followed every time code is written in the future, put it in spec. - -## Local Customization Points - -| Need | Edit location | -| --- | --- | -| Change maximum journal lines | `max_journal_lines` in `.trellis/config.yaml`. | -| Change session auto-commit message | `session_commit_message` in `.trellis/config.yaml`. | -| Change session content format | `.trellis/scripts/add_session.py`. | -| Change how workspace is displayed in context | `.trellis/scripts/common/session_context.py`. | - -## AI Usage Rules - -The AI should not treat workspace as the only source of truth. When resuming a task, read the current task first, then use workspace for background. After a task is complete, record important process notes in workspace; if long-term rules emerged, update spec. diff --git a/.claude/skills/trellis-meta/references/platform-files/agents.md b/.claude/skills/trellis-meta/references/platform-files/agents.md deleted file mode 100644 index efbacfa..0000000 --- a/.claude/skills/trellis-meta/references/platform-files/agents.md +++ /dev/null @@ -1,79 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md deleted file mode 100644 index 94156a8..0000000 --- a/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/platform-files/overview.md b/.claude/skills/trellis-meta/references/platform-files/overview.md deleted file mode 100644 index 60ae1df..0000000 --- a/.claude/skills/trellis-meta/references/platform-files/overview.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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/.claude/skills/trellis-meta/references/platform-files/platform-map.md b/.claude/skills/trellis-meta/references/platform-files/platform-map.md deleted file mode 100644 index b5576f4..0000000 --- a/.claude/skills/trellis-meta/references/platform-files/platform-map.md +++ /dev/null @@ -1,74 +0,0 @@ -# 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. diff --git a/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md deleted file mode 100644 index 816c666..0000000 --- a/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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/.claude/skills/trellis-spec-bootstarp/SKILL.md b/.claude/skills/trellis-spec-bootstarp/SKILL.md deleted file mode 100644 index 2f7c7ad..0000000 --- a/.claude/skills/trellis-spec-bootstarp/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -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/.claude/skills/trellis-spec-bootstarp/references/mcp-setup.md b/.claude/skills/trellis-spec-bootstarp/references/mcp-setup.md deleted file mode 100644 index 629fcbd..0000000 --- a/.claude/skills/trellis-spec-bootstarp/references/mcp-setup.md +++ /dev/null @@ -1,90 +0,0 @@ -# MCP Setup - -GitNexus and ABCoder are recommended when bootstrapping Trellis specs because they expose architecture and AST context to the agent. They are tool choices, not platform requirements. Configure them through whatever MCP mechanism your agent host provides. - -## GitNexus - -GitNexus builds a code knowledge graph from the repository. Use it for module boundaries, execution flows, dependency relationships, blast radius, and graph queries. - -### Install and Index - -```bash -# Run from the repository root. -npx gitnexus analyze - -# Check index status. -npx gitnexus status - -# Re-index after code changes when the analysis is stale. -npx gitnexus analyze -``` - -The index is written to `.gitnexus/`. Keep embeddings only if the project already uses them; otherwise a normal index is enough for spec bootstrapping. - -### MCP Server Command - -Use this server command in the host's MCP configuration: - -```bash -npx -y gitnexus mcp -``` - -### Useful Tools - -| Tool | Purpose | -|------|---------| -| `gitnexus_query` | Find execution flows and functional areas by concept | -| `gitnexus_context` | Inspect callers, callees, references, and process participation for a symbol | -| `gitnexus_impact` | Understand blast radius before changing a symbol | -| `gitnexus_detect_changes` | Check changed symbols and affected flows before finishing | -| `gitnexus_cypher` | Run direct graph queries | -| `gitnexus_list_repos` | List indexed repositories | - -## ABCoder - -ABCoder parses code into UniAST and gives precise package, file, and node-level structure. Use it for signatures, type shapes, implementations, dependencies, and reverse references. - -### Install - -```bash -go install github.com/cloudwego/abcoder@latest -abcoder --help -``` - -### Parse Repositories - -```bash -abcoder parse /absolute/path/to/package \ - --lang typescript \ - --name package-name \ - --output ~/abcoder-asts -``` - -For monorepos, parse each package with a stable `--name` so task notes can reference the same repository names. - -### MCP Server Command - -Use this server command in the host's MCP configuration: - -```bash -abcoder mcp ~/abcoder-asts -``` - -### Useful Tools - -| Tool | Layer | Purpose | -|------|-------|---------| -| `list_repos` | 1 | List parsed repositories | -| `get_repo_structure` | 2 | Inspect packages and files | -| `get_package_structure` | 3 | Inspect nodes within a package | -| `get_file_structure` | 3 | Inspect functions, classes, types, and signatures in a file | -| `get_ast_node` | 4 | Retrieve code, dependencies, references, and implementations | - -## Verification - -After configuration, verify from the agent host that both MCP servers are visible. Then run one simple query against each server before starting the spec writing pass. - -```bash -ls .gitnexus/meta.json -ls ~/abcoder-asts/*.json -``` diff --git a/.claude/skills/trellis-spec-bootstarp/references/repository-analysis.md b/.claude/skills/trellis-spec-bootstarp/references/repository-analysis.md deleted file mode 100644 index 1309d29..0000000 --- a/.claude/skills/trellis-spec-bootstarp/references/repository-analysis.md +++ /dev/null @@ -1,59 +0,0 @@ -# Repository Analysis - -The goal is to discover the project's real architecture before writing rules. Do not start from generic spec templates and fill blanks. Start from the code, then let the spec structure follow. - -## Analysis Order - -1. Read the existing `.trellis/spec/` tree and note which files are templates, outdated, or already project-specific. -2. Inspect package manifests, build scripts, workspace config, and top-level documentation to identify packages and runtime layers. -3. Use GitNexus for execution flows, module clusters, dependency hubs, and impact-sensitive areas. -4. Use ABCoder or language-native tooling for exact signatures, types, class boundaries, and implementation examples. -5. Read representative source and test files directly before turning any finding into a spec rule. - -## What To Capture - -| Area | Questions | -|------|-----------| -| Package boundaries | What does each package own? What imports cross boundaries? | -| Runtime layers | Which code is CLI, backend, frontend, worker, shared library, test-only, or tooling? | -| Core abstractions | Which types, services, stores, commands, routes, or adapters define the system shape? | -| Data flow | Where does user input enter, how is it validated, and where does state persist? | -| Error handling | How are failures represented, logged, surfaced, and tested? | -| Configuration | Where do defaults, environment config, generated files, and templates live? | -| Tests | Which test styles are trusted examples for new work? | - -## GitNexus Usage - -Start broad, then inspect specific symbols: - -```text -gitnexus_query({query: "CLI command execution flow"}) -gitnexus_query({query: "template generation and migration"}) -gitnexus_context({name: "SymbolName"}) -gitnexus_cypher({query: "MATCH (n)-[r]->(m) RETURN n.name, type(r), m.name LIMIT 30"}) -``` - -Use GitNexus results to find important files and flows. Do not quote graph output as the final authority until you have checked the relevant source files. - -## ABCoder Usage - -Use ABCoder when the spec needs exact code shapes: - -```text -list_repos() -get_repo_structure({repo_name: "package-name"}) -get_file_structure({repo_name: "package-name", file_path: "src/example.ts"}) -get_ast_node({repo_name: "package-name", node_ids: [{mod_path: "...", pkg_path: "...", name: "SymbolName"}]}) -``` - -ABCoder is most valuable for documenting constructor patterns, function signatures, type contracts, and reference chains. - -## Analysis Notes - -Keep short notes while analyzing. The notes should include: - -- Package or layer name. -- Files that define the local pattern. -- Rules the spec should teach. -- Anti-patterns found in old code, comments, tests, or migration paths. -- Spec files that should be created, deleted, renamed, or merged. diff --git a/.claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md b/.claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md deleted file mode 100644 index dca2687..0000000 --- a/.claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md +++ /dev/null @@ -1,61 +0,0 @@ -# Spec Task Planning - -Use a single agent as the default execution model. The agent may create Trellis tasks for traceability, but the skill should not require a specific platform, CLI, or parallel worker model. - -## Decomposition - -Create spec work units around real ownership boundaries: - -- One package when a package has its own conventions. -- One layer when the same package has distinct frontend, backend, CLI, worker, or shared-library rules. -- One cross-cutting guide when a pattern spans packages and is not owned by one layer. - -Avoid artificial decomposition. A small library usually needs one focused spec pass, not several tasks. - -## Task Shape - -When a Trellis task is useful, write a concise PRD with these sections: - -```markdown -# Fill <package-or-layer> Trellis Specs - -## Goal -Write project-specific `.trellis/spec/` guidance for <scope>. - -## Scope -- Spec directory: -- Source directories to inspect: -- Tests to inspect: -- Out of scope: - -## Architecture Context -Summarize the concrete findings from repository analysis. - -## Files To Create Or Update -- `.trellis/spec/.../index.md` -- `.trellis/spec/.../<topic>.md` - -## Rules -- Adapt the spec file set to the real codebase. -- Use real source examples with file paths. -- Remove template-only sections that do not apply. -- Do not modify product source code unless the task explicitly asks for it. - -## Acceptance Criteria -- [ ] Specs contain concrete examples and anti-patterns from the repository. -- [ ] No placeholder text remains. -- [ ] Index files match the final spec files. -- [ ] Claims are backed by source files, tests, or project docs. -``` - -## Optional Helper Agents - -If the host supports subagents, helpers can inspect independent packages or run verification. They are optional. The main agent still owns integration and final quality. - -Helper tasks must have clear ownership: - -- Read-only research tasks may inspect any source needed for the assigned scope. -- Write tasks should own disjoint spec directories. -- Verification tasks should check placeholder removal, broken links, and consistency. - -Do not encode helper-agent names, vendor-specific commands, or platform-specific routing in the skill. Put only the required work and acceptance criteria in the task. diff --git a/.claude/skills/trellis-spec-bootstarp/references/spec-writing.md b/.claude/skills/trellis-spec-bootstarp/references/spec-writing.md deleted file mode 100644 index 6bc7dec..0000000 --- a/.claude/skills/trellis-spec-bootstarp/references/spec-writing.md +++ /dev/null @@ -1,70 +0,0 @@ -# Spec Writing - -Trellis specs are coding guidance for future agents. They should explain how to work in this repository, not how a generic project might be organized. - -## Write From Evidence - -Each important rule should be backed by one of these: - -- A source file that demonstrates the preferred pattern. -- A test file that shows expected behavior. -- A project document that defines the convention. -- A repeated pattern across multiple files. - -Use short snippets only when they make the rule clearer. Prefer linking to the file path and naming the symbol or behavior. - -## File Structure - -Keep the spec tree aligned with the project: - -- Keep `index.md` as the navigation file for the spec directory. -- Split topics when developers would look for them independently. -- Merge topics when separate files would repeat the same rule. -- Delete template files that do not apply. -- Add new files for important local patterns the template missed. - -## Content Standards - -Good spec sections include: - -- When the rule applies. -- The local pattern to follow. -- The source or test files that prove the pattern. -- Common mistakes or anti-patterns. -- Verification commands or checks when they are specific and reliable. - -Avoid: - -- Placeholder prose. -- Generic framework advice. -- Tool instructions that only work in one agent host. -- Long copied code blocks. -- Rules based on a single accidental implementation detail. - -## Example Shape - -```markdown -## Command Handlers - -Command handlers should keep argument parsing, validation, and side effects separate. The local pattern is: - -- Parse CLI flags at the command boundary. -- Convert raw inputs into typed task options before invoking core logic. -- Keep filesystem writes in the command or service layer, not in template helpers. - -Reference files: -- `packages/cli/src/commands/example.ts` -- `packages/cli/test/commands/example.test.ts` - -Avoid passing raw `process.argv` or unvalidated config objects into shared helpers. -``` - -## Final Pass - -Before finishing: - -```bash -grep -R "To be filled\\|TODO: fill\\|placeholder" .trellis/spec -``` - -Also check links, index files, and whether any spec still describes a template rather than this repository. diff --git a/.claude/skills/trellis-update-spec/SKILL.md b/.claude/skills/trellis-update-spec/SKILL.md deleted file mode 100644 index 557bc4e..0000000 --- a/.claude/skills/trellis-update-spec/SKILL.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -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/.gitignore b/.gitignore index bf5df08..17206a4 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,11 @@ Thumbs.db # Session runtime continuation cache .omo/ +# AI agent / assistant tooling (local only) +.claude/ +.opencode/ +.trellis/ +AGENTS.md + # Generated Vivado project (create_project.tcl) vivado_prj/ diff --git a/.opencode/agents/trellis-check.md b/.opencode/agents/trellis-check.md deleted file mode 100644 index f76e7a6..0000000 --- a/.opencode/agents/trellis-check.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -description: | - Code quality check expert. Reviews code changes against specs and self-fixes issues. -mode: subagent -permission: - read: allow - write: allow - edit: allow - bash: allow - glob: allow - grep: allow - mcp__exa__*: allow ---- -# Check Agent - -You are the Check Agent in the Trellis workflow. - -## Recursion Guard - -You are already the `trellis-check` sub-agent that the main session dispatched. Do the review and fixes directly. - -- Do NOT spawn another `trellis-check` or `trellis-implement` sub-agent. -- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. -- Only the main session may dispatch Trellis implement/check agents. If more implementation work is needed, report that recommendation instead of spawning. - -## Trellis Context Loading Protocol - -Look for the `<!-- trellis-hook-injected -->` marker in your input above. - -- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the check work directly. -- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>` (or run `python3 ./.trellis/scripts/task.py current --source` as a fallback), then Read `<task-path>/prd.md` and the spec files listed in `<task-path>/check.jsonl` yourself before doing the work. - -## Context - -Before checking, read: -- `.trellis/spec/` - Development guidelines -- Pre-commit checklist for quality standards - -## Core Responsibilities - -1. **Get code changes** - Use git diff to get uncommitted code -2. **Check against specs** - Verify code follows guidelines -3. **Self-fix** - Fix issues yourself, not just report them -4. **Run verification** - typecheck and lint - -## Important - -**Fix issues yourself**, don't just report them. - -You have write and edit tools, you can modify code directly. - ---- - -## Workflow - -### Step 1: Get Changes - -```bash -git diff --name-only # List changed files -git diff # View specific changes -``` - -### Step 2: Check Against Specs - -Read relevant specs in `.trellis/spec/` to check code: - -- Does it follow directory structure conventions -- Does it follow naming conventions -- Does it follow code patterns -- Are there missing types -- Are there potential bugs - -### Step 3: Self-Fix - -After finding issues: - -1. Fix the issue directly (use edit tool) -2. Record what was fixed -3. Continue checking other issues - -### Step 4: Run Verification - -Run project's lint and typecheck commands to verify changes. - -If failed, fix issues and re-run. - ---- - -## Report Format - -```markdown -## Self-Check Complete - -### Files Checked - -- src/components/Feature.tsx -- src/hooks/useFeature.ts - -### Issues Found and Fixed - -1. `<file>:<line>` - <what was fixed> -2. `<file>:<line>` - <what was fixed> - -### Issues Not Fixed - -(If there are issues that cannot be self-fixed, list them here with reasons) - -### Verification Results - -- TypeCheck: Passed -- Lint: Passed - -### Summary - -Checked X files, found Y issues, all fixed. -``` diff --git a/.opencode/agents/trellis-implement.md b/.opencode/agents/trellis-implement.md deleted file mode 100644 index 66977ed..0000000 --- a/.opencode/agents/trellis-implement.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -description: | - Code implementation expert. Understands specs and requirements, then implements features. No git commit allowed. -mode: subagent -permission: - read: allow - write: allow - edit: allow - bash: allow - glob: allow - grep: allow - mcp__exa__*: allow ---- -# Implement Agent - -You are the Implement Agent in the Trellis workflow. - -## Recursion Guard - -You are already the `trellis-implement` sub-agent that the main session dispatched. Do the implementation work directly. - -- Do NOT spawn another `trellis-implement` or `trellis-check` sub-agent. -- If SessionStart context, workflow-state breadcrumbs, or workflow.md say to dispatch `trellis-implement` / `trellis-check`, treat that as a main-session instruction that is already satisfied by your current role. -- Only the main session may dispatch Trellis implement/check agents. If more parallel work is needed, report that recommendation instead of spawning. - -## Trellis Context Loading Protocol - -Look for the `<!-- trellis-hook-injected -->` marker in your input above. - -- **If the marker is present**: prd / spec / research files have already been auto-loaded for you above. Proceed with the implementation work directly. -- **If the marker is absent**: hook injection didn't fire (Windows + Claude Code, `--continue` resume, fork distribution, hooks disabled, etc.). Find the active task path from your dispatch prompt's first line `Active task: <path>` (or run `python3 ./.trellis/scripts/task.py current --source` as a fallback), then Read `<task-path>/prd.md`, `<task-path>/info.md` (if it exists), and the spec files listed in `<task-path>/implement.jsonl` yourself before doing the work. - -## Context - -Before implementing, read: -- `.trellis/workflow.md` - Project workflow -- `.trellis/spec/` - Development guidelines -- Task `prd.md` - Requirements document -- Task `info.md` - Technical design (if exists) - -## Core Responsibilities - -1. **Understand specs** - Read relevant spec files in `.trellis/spec/` -2. **Understand requirements** - Read prd.md and info.md -3. **Implement features** - Write code following specs and design -4. **Self-check** - Ensure code quality -5. **Report results** - Report completion status - -## Forbidden Operations - -**Do NOT execute these git commands:** - -- `git commit` -- `git push` -- `git merge` - ---- - -## Workflow - -### 1. Understand Specs - -Read relevant specs based on task type: - -- Spec layers: `.trellis/spec/<package>/<layer>/` -- Shared guides: `.trellis/spec/guides/` -- Guides: `.trellis/spec/guides/` - -### 2. Understand Requirements - -Read the task's prd.md and info.md: - -- What are the core requirements -- Key points of technical design -- Which files to modify/create - -### 3. Implement Features - -- Write code following specs and technical design -- Follow existing code patterns -- Only do what's required, no over-engineering - -### 4. Verify - -Run project's lint and typecheck commands to verify changes. - ---- - -## Report Format - -```markdown -## Implementation Complete - -### Files Modified - -- `src/components/Feature.tsx` - New component -- `src/hooks/useFeature.ts` - New hook - -### Implementation Summary - -1. Created Feature component... -2. Added useFeature hook... - -### Verification Results - -- Lint: Passed -- TypeCheck: Passed -``` - ---- - -## Code Standards - -- Follow existing code patterns -- Don't add unnecessary abstractions -- Only do what's required, no over-engineering -- Keep code readable diff --git a/.opencode/agents/trellis-research.md b/.opencode/agents/trellis-research.md deleted file mode 100644 index 4efd336..0000000 --- a/.opencode/agents/trellis-research.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -description: | - Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory. -mode: subagent -permission: - read: allow - write: allow - edit: allow - bash: allow - glob: allow - grep: allow - mcp__exa__*: allow - mcp__chrome-devtools__*: allow ---- -# Research Agent - -You are the Research Agent in the Trellis workflow. - -## Core Principle - -**You do one thing: find, explain, and PERSIST information.** - -Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session. - ---- - -## Core Responsibilities - -1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read) -2. **External Search** — library docs, API references, best practices (web search) -3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md` -4. **Report** — return file paths + one-line summaries to the main agent (not full content) - ---- - -## Workflow - -### Step 1: Resolve Current Task - -Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess. - -Ensure `{TASK_DIR}/research/` exists: - -```bash -mkdir -p <TASK_DIR>/research -``` - -### Step 2: Understand Search Request - -Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison). - -### Step 3: Execute Search - -Run independent searches in parallel (Glob + Grep + web) for efficiency. - -### Step 4: Persist Each Topic - -For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below. - -### Step 5: Report to Main Agent - -Reply with ONLY: - -- List of files written (paths relative to repo root) -- One-line summary per file -- Any critical caveats that the main agent needs to know right now - -Do NOT paste full research content into the reply. The files are the contract. - ---- - -## Scope Limits (Strict) - -### Write ALLOWED - -- `{TASK_DIR}/research/*.md` — your own output -- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`) - -### Write FORBIDDEN - -- Code files (`src/`, `lib/`, …) -- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead -- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, `.opencode/`, etc.) -- Other task directories -- Any git operation (commit / push / branch / merge) - -If the user asks you to edit code, decline and suggest spawning `implement` instead. - ---- - -## File Format - -Each `{TASK_DIR}/research/<topic>.md` should follow: - -```markdown -# Research: <topic> - -- **Query**: <original query> -- **Scope**: <internal / external / mixed> -- **Date**: <YYYY-MM-DD> - -## Findings - -### Files Found - -| File Path | Description | -|---|---| -| `src/services/xxx.ts` | Main implementation | -| `src/types/xxx.ts` | Type definitions | - -### Code Patterns - -<describe patterns, cite file:line> - -### External References - -- [Library X docs](url) — <why relevant, version constraints> - -### Related Specs - -- `.trellis/spec/xxx.md` — <description> - -## Caveats / Not Found - -<anything incomplete or uncertain> -``` - ---- - -## Guidelines - -### DO - -- Provide specific file paths and line numbers -- Quote actual code snippets -- Persist every topic to its own file -- Return file paths in your reply, not the full content -- Mark "not found" explicitly when searches come up empty - -### DON'T - -- Don't write code or modify files outside `{TASK_DIR}/research/` -- Don't guess uncertain info -- Don't paste full research text into the reply (files are the deliverable) -- Don't propose improvements or critique implementation (that's not your role) diff --git a/.opencode/commands/trellis/continue.md b/.opencode/commands/trellis/continue.md deleted file mode 100644 index 45eff66..0000000 --- a/.opencode/commands/trellis/continue.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 <X.X> --platform opencode -``` - -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/.opencode/commands/trellis/finish-work.md b/.opencode/commands/trellis/finish-work.md deleted file mode 100644 index ab751c6..0000000 --- a/.opencode/commands/trellis/finish-work.md +++ /dev/null @@ -1,66 +0,0 @@ -# Finish Work - -Wrap up the current session: archive the active task (and any other completed-but-unarchived tasks the user wants to clean up) and record the session journal. Code commits are NOT done here — those happen in workflow Phase 3.4 before you invoke this command. - -## Step 1: Survey current state - -```bash -python3 ./.trellis/scripts/get_context.py --mode record -``` - -This prints: - -- **My active tasks** — review whether any besides the current one are actually done (code merged, AC met) and should be archived this round. -- **Git status** — quick visual on what's dirty. -- **Recent commits** — you'll need their hashes in Step 4 for `--commit`. - -If `--mode record` surfaces other completed tasks not tied to the current session, surface them to the user with a one-shot confirmation: "These N tasks look done — archive them too in this round? [y/N]". Default is no; the current active task is always archived in Step 3 regardless. - -## Step 2: Sanity check — classify dirty paths - -Run: - -```bash -git status --porcelain -``` - -Filter out paths under `.trellis/workspace/` and `.trellis/tasks/` — those are managed by `add_session.py` and `task.py archive` auto-commits and will appear dirty as part of this skill's own work. - -For each remaining dirty path, decide whether it belongs to **the current task** or to **other parallel work** (e.g., another terminal window editing the same repo). Heuristics: - -- Paths referenced in the current task's `prd.md` / `implement.jsonl` / `check.jsonl` → current task -- Paths in code areas matching the task's stated scope, or that you remember editing this session → current task -- Paths in unrelated areas you have no recollection of touching this session → other parallel work - -Then route: - -- **Any remaining path looks like current-task work** — bail out with: - > "Working tree has uncommitted code changes from this task: `<list>`. Return to workflow Phase 3.4 to commit them before running `/trellis:finish-work`." - - Do NOT run `git commit` here. Do NOT prompt the user to commit. The user goes back to Phase 3.4 and the AI drives the batched commit there. -- **All remaining paths look unrelated** (other parallel-window work) — report them once and continue to Step 3: - > "FYI, dirty files outside this task's scope — leaving them for the other window: `<list>`." -- **Genuinely unsure** — ask the user once: "Are `<list>` this task's work I forgot to commit, or another window's? (commit / ignore)" — then route per their answer. - -## Step 3: Archive task(s) - -```bash -python3 ./.trellis/scripts/task.py archive <task-name> -``` - -At minimum: the current active task (if any). Plus any extra tasks the user confirmed in Step 1. Each archive produces a `chore(task): archive ...` commit via the script's auto-commit. - -If there is no active task and the user did not confirm any cleanup archives, skip this step. - -## Step 4: Record session journal - -```bash -python3 ./.trellis/scripts/add_session.py \ - --title "Session Title" \ - --commit "hash1,hash2" \ - --summary "Brief summary" -``` - -Use the work-commit hashes produced in Phase 3.4 (visible in Step 1's `Recent commits` list, or via `git log --oneline`) for `--commit`. Do not include the archive commit hashes from Step 3. This produces a `chore: record journal` commit. - -Final git log order: `<work commits from 3.4>` → `chore(task): archive ...` (one or more) → `chore: record journal`. diff --git a/.opencode/lib/session-utils.js b/.opencode/lib/session-utils.js deleted file mode 100644 index e80fa8d..0000000 --- a/.opencode/lib/session-utils.js +++ /dev/null @@ -1,432 +0,0 @@ -/* global process */ -import { existsSync, readFileSync, readdirSync, statSync } from "fs" -import { basename, join } from "path" -import { execFileSync } from "child_process" -import { platform } from "os" -import { debugLog } from "./trellis-context.js" - -const PYTHON_CMD = platform() === "win32" ? "python" : "python3" - -const FIRST_REPLY_NOTICE = `<first-reply-notice> -On the first visible assistant reply in this session, begin with exactly one short Chinese sentence: -Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。 -Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session. -</first-reply-notice>` - -function hasCuratedJsonlEntry(jsonlPath) { - try { - const content = readFileSync(jsonlPath, "utf-8") - for (const rawLine of content.split(/\r?\n/)) { - const line = rawLine.trim() - if (!line) continue - try { - const row = JSON.parse(line) - if (row && typeof row === "object" && typeof row.file === "string" && row.file) { - return true - } - } catch { - // Ignore malformed line - } - } - } catch { - return false - } - return false -} - -function getTaskStatus(ctx, platformInput = null) { - const active = ctx.getActiveTask(platformInput) - const taskRef = active.taskPath - if (!taskRef) { - return `Status: NO ACTIVE TASK\nSource: ${active.source}\nNext: Describe what you want to work on` - } - - const taskDir = ctx.resolveTaskDir(taskRef) - - if (active.stale || !taskDir || !existsSync(taskDir)) { - return `Status: STALE POINTER\nTask: ${taskRef}\nSource: ${active.source}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish` - } - - let taskData = {} - const taskJsonPath = join(taskDir, "task.json") - if (existsSync(taskJsonPath)) { - try { - taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8")) - } catch { - // Ignore parse errors - } - } - - const taskTitle = taskData.title || taskRef - const taskStatus = taskData.status || "unknown" - - if (taskStatus === "completed") { - const dirName = basename(taskDir) - return `Status: COMPLETED\nTask: ${taskTitle}\nSource: ${active.source}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task` - } - - let hasContext = false - for (const jsonlName of ["implement.jsonl", "check.jsonl"]) { - const jsonlPath = join(taskDir, jsonlName) - if (existsSync(jsonlPath) && hasCuratedJsonlEntry(jsonlPath)) { - hasContext = true - break - } - } - - const hasPrd = existsSync(join(taskDir, "prd.md")) - - if (!hasPrd) { - return `Status: NOT READY\nTask: ${taskTitle}\nSource: ${active.source}\nMissing: prd.md not created\nNext: Write PRD (see workflow.md Phase 1.1) then curate implement.jsonl per Phase 1.3` - } - - if (!hasContext) { - return `Status: NOT READY\nTask: ${taskTitle}\nSource: ${active.source}\nMissing: implement.jsonl / check.jsonl missing or empty\nNext: Curate entries per workflow.md Phase 1.3 (spec + research files only), then \`task.py start\`` - } - - return ( - `Status: READY\nTask: ${taskTitle}\n` + - `Source: ${active.source}\n` + - "Next required action: dispatch `trellis-implement` per Phase 2.1. " + - "For agent-capable platforms, the default is to NOT edit code in the main session. " + - "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n" + - "User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the " + - "main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / " + - "\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. " + - "Per-turn only; do NOT invent an override the user did not say." - ) -} - -function loadTrellisConfig(directory, contextKey = null) { - const scriptPath = join(directory, ".trellis", "scripts", "get_context.py") - if (!existsSync(scriptPath)) { - return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null } - } - try { - const output = execFileSync(PYTHON_CMD, [scriptPath, "--mode", "packages", "--json"], { - cwd: directory, - timeout: 5000, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - env: { - ...process.env, - ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}), - }, - }) - const data = JSON.parse(output) - if (data.mode !== "monorepo") { - return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null } - } - const pkgDict = {} - for (const pkg of (data.packages || [])) { - pkgDict[pkg.name] = pkg - } - return { - isMonorepo: true, - packages: pkgDict, - specScope: data.specScope || null, - activeTaskPackage: data.activeTaskPackage || null, - defaultPackage: data.defaultPackage || null, - } - } catch (e) { - debugLog("session", "loadTrellisConfig error:", e.message) - return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null } - } -} - -function checkLegacySpec(directory, config) { - if (!config.isMonorepo || Object.keys(config.packages).length === 0) { - return null - } - - const specDir = join(directory, ".trellis", "spec") - if (!existsSync(specDir)) return null - - let hasLegacy = false - for (const name of ["backend", "frontend"]) { - if (existsSync(join(specDir, name, "index.md"))) { - hasLegacy = true - break - } - } - if (!hasLegacy) return null - - const pkgNames = Object.keys(config.packages).sort() - const missing = pkgNames.filter(name => !existsSync(join(specDir, name))) - - if (missing.length === 0) return null - - if (missing.length === pkgNames.length) { - return ( - `[!] Legacy spec structure detected: found \`spec/backend/\` or \`spec/frontend/\` ` + - `but no package-scoped \`spec/<package>/\` directories.\n` + - `Monorepo packages: ${pkgNames.join(", ")}\n` + - `Please reorganize: \`spec/backend/\` -> \`spec/<package>/backend/\`` - ) - } - return ( - `[!] Partial spec migration detected: packages ${missing.join(", ")} ` + - `still missing \`spec/<pkg>/\` directory.\n` + - `Please complete migration for all packages.` - ) -} - -function resolveSpecScope(config) { - if (!config.isMonorepo || Object.keys(config.packages).length === 0) { - return null - } - - const { specScope, activeTaskPackage, defaultPackage, packages } = config - if (specScope == null) return null - - if (specScope === "active_task") { - if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage]) - if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage]) - return null - } - - if (Array.isArray(specScope)) { - const valid = new Set() - for (const entry of specScope) { - if (entry in packages) { - valid.add(entry) - } - } - if (valid.size > 0) return valid - if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage]) - if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage]) - return null - } - - return null -} - -export function buildSessionContext(ctx, platformInput = null) { - const directory = ctx.directory - const trellisDir = join(directory, ".trellis") - const contextKey = typeof ctx.getContextKey === "function" - ? ctx.getContextKey(platformInput) - : null - - const config = loadTrellisConfig(directory, contextKey) - const allowedPkgs = resolveSpecScope(config) - - const parts = [] - - parts.push(`<trellis-context> -You are starting a new session in a Trellis-managed project. -Read and follow all instructions below carefully. -</trellis-context>`) - parts.push(FIRST_REPLY_NOTICE) - - const legacyWarning = checkLegacySpec(directory, config) - if (legacyWarning) { - parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`) - } - - const contextScript = join(trellisDir, "scripts", "get_context.py") - if (existsSync(contextScript)) { - const output = ctx.runScript(contextScript, undefined, contextKey) - if (output) { - parts.push("<current-state>") - parts.push(output) - parts.push("</current-state>") - } - } - - const workflowContent = ctx.readProjectFile(".trellis/workflow.md") - if (workflowContent) { - const allLines = workflowContent.split("\n") - const overviewLines = [ - "# Development Workflow — Section Index", - "Full guide: .trellis/workflow.md (read on demand)", - "", - "## Table of Contents", - ] - for (const line of allLines) { - if (line.startsWith("## ")) overviewLines.push(line) - } - overviewLines.push("", "---", "") - - let rangeStart = -1 - let rangeEnd = allLines.length - for (let i = 0; i < allLines.length; i++) { - const stripped = allLines[i].trim() - if (rangeStart === -1 && stripped === "## Phase Index") { - rangeStart = i - } else if (rangeStart !== -1 && stripped === "## Workflow State Breadcrumbs") { - rangeEnd = i - break - } - } - if (rangeStart !== -1) { - overviewLines.push(...allLines.slice(rangeStart, rangeEnd)) - } - - parts.push("<workflow>") - parts.push(overviewLines.join("\n").trimEnd()) - parts.push("</workflow>") - } - - parts.push("<guidelines>") - parts.push( - "Project spec indexes are listed by path below. Each index contains a " + - "**Pre-Development Checklist** listing the specific guideline files to " + - "read before coding.\n\n" + - "- If you're spawning an implement/check sub-agent, context is injected " + - "automatically via `{task}/implement.jsonl` / `check.jsonl`. You do NOT " + - "need to read these indexes yourself.\n" + - "- For agent-capable platforms, do NOT edit code directly in the main " + - "session; dispatch `trellis-implement` and `trellis-check` so JSONL " + - "context is loaded by the sub-agents.\n" - ) - - const specDir = join(directory, ".trellis", "spec") - - const guidesIndex = join(specDir, "guides", "index.md") - if (existsSync(guidesIndex)) { - const content = ctx.readFile(guidesIndex) - if (content) { - parts.push(`## guides (inlined — cross-package thinking guides)\n${content}\n`) - } - } - - const paths = [] - if (existsSync(specDir)) { - try { - const subs = readdirSync(specDir).filter(name => { - if (name.startsWith(".")) return false - try { - return statSync(join(specDir, name)).isDirectory() - } catch { - return false - } - }).sort() - - for (const sub of subs) { - if (sub === "guides") continue - - const indexFile = join(specDir, sub, "index.md") - if (existsSync(indexFile)) { - paths.push(`.trellis/spec/${sub}/index.md`) - } else { - if (allowedPkgs !== null && !allowedPkgs.has(sub)) continue - try { - const nested = readdirSync(join(specDir, sub)).filter(name => { - try { - return statSync(join(specDir, sub, name)).isDirectory() - } catch { - return false - } - }).sort() - for (const layer of nested) { - const nestedIndex = join(specDir, sub, layer, "index.md") - if (existsSync(nestedIndex)) { - paths.push(`.trellis/spec/${sub}/${layer}/index.md`) - } - } - } catch { - // Ignore directory read errors - } - } - } - } catch { - // Ignore spec directory read errors - } - } - - if (paths.length > 0) { - parts.push("## Available spec indexes (read on demand)") - for (const p of paths) { - parts.push(`- ${p}`) - } - parts.push("") - } - - parts.push( - "Discover more via: " + - "`python3 ./.trellis/scripts/get_context.py --mode packages`" - ) - parts.push("</guidelines>") - - const taskStatus = getTaskStatus(ctx, platformInput) - parts.push(`<task-status>\n${taskStatus}\n</task-status>`) - - parts.push(`<ready> -Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them. -When the user sends the first message, follow <task-status> and the workflow guide. -If a task is READY, execute its Next required action without asking whether to continue. -</ready>`) - - return parts.join("\n\n") -} - -function getTrellisMetadata(metadata) { - if (!metadata || typeof metadata !== "object") { - return {} - } - - const trellis = metadata.trellis - if (!trellis || typeof trellis !== "object") { - return {} - } - - return trellis -} - -function markPartAsSessionStart(part) { - const metadata = part.metadata && typeof part.metadata === "object" - ? part.metadata - : {} - part.metadata = { - ...metadata, - trellis: { - ...getTrellisMetadata(metadata), - sessionStart: true, - }, - } -} - -function hasSessionStartMarker(part) { - if (!part || part.type !== "text" || typeof part.text !== "string") { - return false - } - - return getTrellisMetadata(part.metadata).sessionStart === true -} - -export function hasInjectedTrellisContext(messages) { - if (!Array.isArray(messages)) { - return false - } - - return messages.some(message => { - if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) { - return false - } - - return message.parts.some(hasSessionStartMarker) - }) -} - -export async function hasPersistedInjectedContext(client, directory, sessionID) { - try { - const response = await client.session.messages({ - path: { id: sessionID }, - query: { directory }, - throwOnError: true, - }) - return hasInjectedTrellisContext(response.data || []) - } catch (error) { - debugLog( - "session", - "Failed to read session history for dedupe:", - error instanceof Error ? error.message : String(error), - ) - return false - } -} - -export function markContextInjected(part) { - markPartAsSessionStart(part) -} diff --git a/.opencode/lib/trellis-context.js b/.opencode/lib/trellis-context.js deleted file mode 100644 index 27ccbf4..0000000 --- a/.opencode/lib/trellis-context.js +++ /dev/null @@ -1,381 +0,0 @@ -/** - * Trellis Context Manager - * - * Utility class for OpenCode plugins providing file reading, - * JSONL parsing, and context building capabilities. - */ - -import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs" -import { isAbsolute, join } from "path" -import { platform } from "os" -import { execSync } from "child_process" -import { createHash } from "crypto" -import process from "process" - -const PYTHON_CMD = platform() === "win32" ? "python" : "python3" -// Debug logging -const DEBUG_LOG = "/tmp/trellis-plugin-debug.log" - -function debugLog(prefix, ...args) { - const timestamp = new Date().toISOString() - const msg = `[${timestamp}] [${prefix}] ${args.map(a => typeof a === "object" ? JSON.stringify(a) : a).join(" ")}\n` - try { - appendFileSync(DEBUG_LOG, msg) - } catch { - // ignore - } -} - -function stringValue(value) { - return typeof value === "string" && value.trim() ? value.trim() : null -} - -function sanitizeKey(raw) { - const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, "") - return safe ? safe.slice(0, 160) : "" -} - -function hashValue(raw) { - return createHash("sha256").update(raw).digest("hex").slice(0, 24) -} - -function lookupString(data, keys) { - if (!data || typeof data !== "object") return null - for (const key of keys) { - const value = stringValue(data[key]) - if (value) return value - } - for (const nestedKey of ["input", "properties", "event", "hook_input", "hookInput"]) { - const nested = data[nestedKey] - if (nested && typeof nested === "object") { - const value = lookupString(nested, keys) - if (value) return value - } - } - return null -} - -function buildContextKey(platformName, kind, value) { - if (kind === "transcript") { - return `${platformName}_transcript_${hashValue(value)}` - } - const safeValue = sanitizeKey(value) - return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}` -} - -// Matches `trellis-implement`, `trellis-check`, `trellis-research` exactly. -// Used by chat.message plugins to skip injection inside Trellis sub-agent turns. -const TRELLIS_SUBAGENT_RE = /^trellis-(implement|check|research)$/ - -/** - * Return true when the OpenCode `chat.message` input represents a Trellis - * sub-agent turn. `input.agent` is set by OpenCode when a Task tool spawns a - * child session with a custom agent (see `packages/opencode/src/tool/task.ts`). - */ -export function isTrellisSubagent(input) { - if (!input || typeof input !== "object") return false - const agent = typeof input.agent === "string" ? input.agent.trim() : "" - return TRELLIS_SUBAGENT_RE.test(agent) -} - -/** - * Trellis Context Manager - */ -export class TrellisContext { - constructor(directory) { - this.directory = directory - debugLog("context", "TrellisContext initialized", { directory }) - } - - // ============================================================ - // Trellis Project Detection - // ============================================================ - - isTrellisProject() { - return existsSync(join(this.directory, ".trellis")) - } - - getContextKey(platformInput = null) { - const override = stringValue(process.env.TRELLIS_CONTEXT_ID) - if (override) { - return sanitizeKey(override) || hashValue(override) - } - - const runID = stringValue(process.env.OPENCODE_RUN_ID) - if (runID) return buildContextKey("opencode", "session", runID) - - const input = platformInput && typeof platformInput === "object" ? platformInput : null - if (!input) return null - - const sessionID = lookupString(input, ["session_id", "sessionId", "sessionID"]) - if (sessionID) return buildContextKey("opencode", "session", sessionID) - - const conversationID = lookupString(input, ["conversation_id", "conversationId", "conversationID"]) - if (conversationID) return buildContextKey("opencode", "conversation", conversationID) - - const transcriptPath = lookupString(input, ["transcript_path", "transcriptPath", "transcript"]) - if (transcriptPath) return buildContextKey("opencode", "transcript", transcriptPath) - - return null - } - - readContext(contextKey) { - try { - const contextPath = join(this.directory, ".trellis", ".runtime", "sessions", `${contextKey}.json`) - if (!existsSync(contextPath)) return null - return JSON.parse(readFileSync(contextPath, "utf-8")) - } catch { - return null - } - } - - /** - * Get active task from session runtime context. - * - * Resolution order (mirrors Python `active_task.resolve_active_task`): - * 1. Lookup the runtime file for the input-derived context key. - * 2. If that misses and exactly one session runtime file exists locally, - * use it (`_resolveSingleSessionFallback`). Refuses to guess when 0 or - * ≥2 files exist so multi-window isolation holds. - */ - getActiveTask(platformInput = null) { - const contextKey = this.getContextKey(platformInput) - if (contextKey) { - const context = this.readContext(contextKey) - const taskRef = this.normalizeTaskRef(context?.current_task || "") - if (taskRef) { - const taskDir = this.resolveTaskDir(taskRef) - return { - taskPath: taskRef, - source: `session:${contextKey}`, - stale: !taskDir || !existsSync(taskDir), - } - } - } - - const fallback = this._resolveSingleSessionFallback() - if (fallback) { - return fallback - } - - return { taskPath: null, source: "none", stale: false } - } - - /** - * Mirror of Python `_resolve_single_session_fallback`. Returns the task - * pointed at by the sole session runtime file when exactly one exists, - * else null. - */ - _resolveSingleSessionFallback() { - const sessionsDir = join(this.directory, ".trellis", ".runtime", "sessions") - if (!existsSync(sessionsDir)) return null - - let files - try { - files = readdirSync(sessionsDir) - .filter(name => name.endsWith(".json")) - .sort() - } catch { - return null - } - if (files.length !== 1) return null - - const sessionFile = join(sessionsDir, files[0]) - let context - try { - context = JSON.parse(readFileSync(sessionFile, "utf-8")) - } catch { - return null - } - const taskRef = this.normalizeTaskRef(context?.current_task || "") - if (!taskRef) return null - - const taskDir = this.resolveTaskDir(taskRef) - const fallbackKey = files[0].replace(/\.json$/, "") - return { - taskPath: taskRef, - source: `session-fallback:${fallbackKey}`, - stale: !taskDir || !existsSync(taskDir), - } - } - - getCurrentTask(platformInput = null) { - return this.getActiveTask(platformInput).taskPath - } - - normalizeTaskRef(taskRef) { - if (!taskRef) { - return "" - } - - if (isAbsolute(taskRef)) { - return taskRef.trim() - } - - let normalized = taskRef.trim().replace(/\\/g, "/") - while (normalized.startsWith("./")) { - normalized = normalized.slice(2) - } - - if (normalized.startsWith("tasks/")) { - return `.trellis/${normalized}` - } - - return normalized - } - - resolveTaskDir(taskRef) { - const normalized = this.normalizeTaskRef(taskRef) - if (!normalized) { - return null - } - - if (isAbsolute(normalized)) { - return normalized - } - - if (normalized.startsWith(".trellis/")) { - return join(this.directory, normalized) - } - - return join(this.directory, ".trellis", "tasks", normalized) - } - - // ============================================================ - // File Reading Utilities - // ============================================================ - - readFile(filePath) { - try { - if (existsSync(filePath)) { - return readFileSync(filePath, "utf-8") - } - } catch { - // Ignore read errors - } - return null - } - - readProjectFile(relativePath) { - return this.readFile(join(this.directory, relativePath)) - } - - runScript(scriptPath, cwd = null, contextKey = null) { - try { - const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, { - cwd: cwd || this.directory, - timeout: 10000, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - env: { - ...process.env, - ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}), - }, - }) - return result || "" - } catch { - return "" - } - } - - // ============================================================ - // JSONL Reading - // ============================================================ - - readDirectoryMdFiles(dirPath, maxFiles = 20) { - const results = [] - const fullPath = join(this.directory, dirPath) - - if (!existsSync(fullPath)) { - return results - } - - try { - const files = readdirSync(fullPath) - .filter(f => f.endsWith(".md")) - .sort() - .slice(0, maxFiles) - - for (const filename of files) { - const filePath = join(dirPath, filename) - const content = this.readProjectFile(filePath) - if (content) { - results.push({ path: filePath, content }) - } - } - } catch { - // Ignore directory read errors - } - - return results - } - - /** - * Read a JSONL file and load referenced files/directories - * Supports: - * {"file": "path/to/file.md", "reason": "..."} - * {"file": "path/to/dir/", "type": "directory", "reason": "..."} - */ - readJsonlWithFiles(jsonlPath) { - const results = [] - const content = this.readFile(jsonlPath) - if (!content) return results - - for (const line of content.split("\n")) { - if (!line.trim()) continue - try { - const item = JSON.parse(line) - const file = item.file || item.path - const entryType = item.type || "file" - - if (!file) continue - - if (entryType === "directory") { - const dirEntries = this.readDirectoryMdFiles(file) - results.push(...dirEntries) - } else { - const fullPath = join(this.directory, file) - const fileContent = this.readFile(fullPath) - if (fileContent) { - results.push({ path: file, content: fileContent }) - } - } - } catch { - // Ignore parse errors for individual lines - } - } - return results - } - - buildContextFromEntries(entries) { - return entries.map(e => `=== ${e.path} ===\n${e.content}`).join("\n\n") - } -} - -// ============================================================ -// Context Collector (for session deduplication) -// ============================================================ - -class ContextCollector { - constructor() { - this.processed = new Set() - } - - markProcessed(sessionID) { - this.processed.add(sessionID) - } - - isProcessed(sessionID) { - return this.processed.has(sessionID) - } - - clear(sessionID) { - this.processed.delete(sessionID) - } -} - -// Singleton instance -export const contextCollector = new ContextCollector() - -// Export debug log for plugins -export { debugLog } diff --git a/.opencode/package.json b/.opencode/package.json deleted file mode 100644 index 1e3ea8f..0000000 --- a/.opencode/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "@opencode-ai/plugin": "^1.14.39" - } -} \ No newline at end of file diff --git a/.opencode/plugins/inject-subagent-context.js b/.opencode/plugins/inject-subagent-context.js deleted file mode 100644 index 2a47bbc..0000000 --- a/.opencode/plugins/inject-subagent-context.js +++ /dev/null @@ -1,497 +0,0 @@ -/* global process */ -/** - * Trellis Context Injection Plugin - * - * Injects context when Task tool is called with supported subagent types. - * Uses OpenCode's tool.execute.before hook. - */ - -import { existsSync, readdirSync } from "fs" -import { join } from "path" -import { TrellisContext, debugLog } from "../lib/trellis-context.js" - -// Supported subagent types -const AGENTS_ALL = ["implement", "check", "research"] -const AGENTS_REQUIRE_TASK = ["implement", "check"] - -// Match `Active task: <path>` on the first non-empty line of the dispatch -// prompt. Mirrors the contract in workflow.md's [workflow-state:in_progress] -// breadcrumb so multi-window users can disambiguate which task is targeted. -const ACTIVE_TASK_HINT_RE = /^\s*Active task:\s*(\S+)\s*$/m - -function extractActiveTaskHint(prompt) { - if (typeof prompt !== "string" || !prompt) return null - const match = prompt.match(ACTIVE_TASK_HINT_RE) - return match ? match[1].trim() : null -} - -/** - * Get context for implement agent. `taskDir` may be relative - * (`.trellis/tasks/foo`) or absolute; both are resolved via - * `ctx.resolveTaskDir`. - */ -function getImplementContext(ctx, taskDir) { - const parts = [] - const taskDirFull = ctx.resolveTaskDir(taskDir) - if (!taskDirFull) return "" - - const jsonlPath = join(taskDirFull, "implement.jsonl") - const entries = ctx.readJsonlWithFiles(jsonlPath) - if (entries.length > 0) { - parts.push(ctx.buildContextFromEntries(entries)) - } - - const prd = ctx.readFile(join(taskDirFull, "prd.md")) - if (prd) { - parts.push(`=== ${taskDir}/prd.md (Requirements) ===\n${prd}`) - } - - const info = ctx.readFile(join(taskDirFull, "info.md")) - if (info) { - parts.push(`=== ${taskDir}/info.md (Technical Design) ===\n${info}`) - } - - return parts.join("\n\n") -} - -/** - * Get context for check agent. `taskDir` may be relative or absolute. - */ -function getCheckContext(ctx, taskDir) { - const parts = [] - const taskDirFull = ctx.resolveTaskDir(taskDir) - if (!taskDirFull) return "" - - const jsonlPath = join(taskDirFull, "check.jsonl") - const entries = ctx.readJsonlWithFiles(jsonlPath) - if (entries.length > 0) { - parts.push(ctx.buildContextFromEntries(entries)) - } - - const prd = ctx.readFile(join(taskDirFull, "prd.md")) - if (prd) { - parts.push(`=== ${taskDir}/prd.md (Requirements) ===\n${prd}`) - } - - return parts.join("\n\n") -} - -/** - * Get context for finish phase (final check before PR) - */ -function getFinishContext(ctx, taskDir) { - // Finish reuses check context (same JSONL source) - return getCheckContext(ctx, taskDir) -} - - -/** - * Get context for research agent - */ -function getResearchContext(ctx) { - const parts = [] - - // Dynamic project structure (scan actual spec directory) - const specPath = ".trellis/spec" - const specFull = join(ctx.directory, specPath) - - const structureLines = [`## Project Spec Directory Structure\n\n\`\`\`\n${specPath}/`] - if (existsSync(specFull)) { - try { - const entries = readdirSync(specFull, { withFileTypes: true }) - .filter(d => d.isDirectory() && !d.name.startsWith(".")) - .sort((a, b) => a.name.localeCompare(b.name)) - - for (const entry of entries) { - const entryPath = join(specFull, entry.name) - if (existsSync(join(entryPath, "index.md"))) { - structureLines.push(`├── ${entry.name}/`) - } else { - try { - const nested = readdirSync(entryPath, { withFileTypes: true }) - .filter(d => d.isDirectory() && existsSync(join(entryPath, d.name, "index.md"))) - .sort((a, b) => a.name.localeCompare(b.name)) - if (nested.length > 0) { - structureLines.push(`├── ${entry.name}/`) - for (const n of nested) { - structureLines.push(`│ ├── ${n.name}/`) - } - } - } catch { - // Ignore nested read errors - } - } - } - } catch { - // Ignore read errors - } - } - structureLines.push("```") - - parts.push(structureLines.join("\n") + ` - -## Search Tips - -- Spec files: \`.trellis/spec/**/*.md\` -- Known issues: \`.trellis/big-question/\` -- Code search: Use Glob and Grep tools -- Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa`) - - return parts.join("\n\n") -} - -/** - * Build enhanced prompt with context - */ -function buildPrompt(agentType, originalPrompt, context, isFinish = false) { - const templates = { - implement: `<!-- trellis-hook-injected --> -# Implement Agent Task - -You are the Implement Agent in the Multi-Agent Pipeline. - -## Your Context - -${context} - ---- - -## Your Task - -${originalPrompt} - ---- - -## Workflow - -1. **Understand specs** - All dev specs are injected above -2. **Understand requirements** - Read requirements and technical design -3. **Implement feature** - Follow specs and design -4. **Self-check** - Ensure code quality - -## Important Constraints - -- Do NOT execute git commit -- Follow all dev specs injected above -- Report list of modified/created files when done`, - - check: isFinish ? `<!-- trellis-hook-injected --> -# Finish Agent Task - -You are performing the final check before creating a PR. - -## Your Context - -${context} - ---- - -## Your Task - -${originalPrompt} - ---- - -## Workflow - -1. **Review changes** - Run \`git diff --name-only\` to see all changed files -2. **Verify requirements** - Check each requirement in prd.md is implemented -3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions - - If new pattern/convention found: read target spec file → update it → update index.md if needed - - If infra/cross-layer change: follow the 7-section mandatory template from update-spec.md - - If pure code fix with no new patterns: skip this step -4. **Run final checks** - Execute lint and typecheck -5. **Confirm ready** - Ensure code is ready for PR - -## Important Constraints - -- You MAY update spec files when gaps are detected (use update-spec.md as guide) -- MUST read the target spec file BEFORE editing (avoid duplicating existing content) -- Do NOT update specs for trivial changes (typos, formatting, obvious fixes) -- If critical CODE issues found, report them clearly (fix specs, not code) -- Verify all acceptance criteria in prd.md are met` : - `<!-- trellis-hook-injected --> -# Check Agent Task - -You are the Check Agent in the Multi-Agent Pipeline. - -## Your Context - -${context} - ---- - -## Your Task - -${originalPrompt} - ---- - -## Workflow - -1. **Get changes** - Run \`git diff --name-only\` and \`git diff\` -2. **Check against specs** - Check item by item -3. **Self-fix** - Fix issues directly, don't just report -4. **Run verification** - Run lint and typecheck - -## Important Constraints - -- Fix issues yourself, don't just report -- Must execute complete checklist`, - - research: `<!-- trellis-hook-injected --> -# Research Agent Task - -You are the Research Agent in the Multi-Agent Pipeline. - -## Core Principle - -**You do one thing: find and explain information.** - -## Project Info - -${context} - ---- - -## Your Task - -${originalPrompt} - ---- - -## Workflow - -1. **Understand query** - Determine search type and scope -2. **Plan search** - List search steps -3. **Execute search** - Run multiple searches in parallel -4. **Organize results** - Output structured report - -## Strict Boundaries - -**Only allowed**: Describe what exists, where it is, how it works - -**Forbidden**: Suggest improvements, criticize implementation, modify files` - } - - return templates[agentType] || originalPrompt -} - -function shellQuote(value) { - return `'${String(value).replace(/'/g, "'\\''")}'` -} - -function powershellQuote(value) { - return `'${String(value).replace(/'/g, "''")}'` -} - -function envValue(env, key) { - const value = env?.[key] - return typeof value === "string" && value.trim() ? value.trim() : null -} - -function shellBasename(value) { - return value.replace(/\\/g, "/").split("/").pop()?.toLowerCase() || "" -} - -function isWindowsPosixShell(env = process.env) { - if (envValue(env, "MSYSTEM")) return true - if (envValue(env, "MINGW_PREFIX")) return true - if (envValue(env, "OPENCODE_GIT_BASH_PATH")) return true - - const ostype = envValue(env, "OSTYPE")?.toLowerCase() || "" - if (/(msys|mingw|cygwin)/.test(ostype)) return true - - const shell = shellBasename(envValue(env, "SHELL") || "") - return /^(bash|sh|zsh)(\.exe)?$/.test(shell) -} - -function buildTrellisContextPrefix(contextKey, hostPlatform = process.platform, env = process.env) { - if (hostPlatform === "win32" && !isWindowsPosixShell(env)) { - return `$env:TRELLIS_CONTEXT_ID = ${powershellQuote(contextKey)}; ` - } - - return `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ` -} - -function getBashCommandKey(args) { - if (!args || typeof args !== "object") return null - if (typeof args.command === "string") return "command" - if (typeof args.cmd === "string") return "cmd" - return null -} - -function commandStartsWithTrellisContext(command) { - const firstCommand = command.trimStart().split(/[;&|]/, 1)[0].trimStart() - return ( - /^TRELLIS_CONTEXT_ID\s*=/.test(firstCommand) || - /^export\s+TRELLIS_CONTEXT_ID\s*=/.test(firstCommand) || - /^env\s+(?:(?:-\S+|[A-Za-z_][A-Za-z0-9_]*=\S*)\s+)*TRELLIS_CONTEXT_ID\s*=/.test(firstCommand) || - /^\$env:TRELLIS_CONTEXT_ID\s*=/i.test(firstCommand) - ) -} - -/** - * OpenCode TUI may not expose OPENCODE_RUN_ID to Bash. The plugin hook still - * receives session identity, so inject it into Bash commands before execution. - */ -function injectTrellisContextIntoBash(ctx, input, output, hostPlatform, env) { - const args = output?.args - const commandKey = getBashCommandKey(args) - if (!commandKey) return false - - const command = args[commandKey] - if (!command.trim()) return false - if (commandStartsWithTrellisContext(command)) return false - - const contextKey = ctx.getContextKey(input) - if (!contextKey) return false - - args[commandKey] = `${buildTrellisContextPrefix(contextKey, hostPlatform, env)}${command}` - return true -} - -// OpenCode plugin factory: `export default async (input) => hooks`. -// OpenCode 1.2.x iterates every module export and invokes it as a function -// (packages/opencode/src/plugin/index.ts — `for ([_, fn] of Object.entries(mod)) await fn(input)`); -// the previous `{ id, server }` object shape failed with -// `TypeError: fn is not a function` in 1.2.x. -export default async ({ directory, platform: hostPlatform = process.platform, env = process.env }) => { - const ctx = new TrellisContext(directory) - debugLog("inject", "Plugin loaded, directory:", directory) - - return { - "tool.execute.before": async (input, output) => { - try { - if (process.env.TRELLIS_HOOKS === "0" || process.env.TRELLIS_DISABLE_HOOKS === "1") { - return - } - debugLog("inject", "tool.execute.before called, tool:", input?.tool) - - const toolName = input?.tool?.toLowerCase() - if (toolName === "bash") { - if (injectTrellisContextIntoBash(ctx, input, output, hostPlatform, env)) { - debugLog("inject", "Injected TRELLIS_CONTEXT_ID into Bash command") - } - return - } - - if (toolName !== "task") { - return - } - - const args = output?.args - if (!args) return - - const rawSubagentType = args.subagent_type - // Strip "trellis-" prefix added by v0.5.0-beta.5 agent rename migration - const subagentType = (rawSubagentType || "").replace(/^trellis-/, "") - const originalPrompt = args.prompt || "" - - debugLog("inject", "Task tool called, subagent_type:", rawSubagentType) - - if (!AGENTS_ALL.includes(subagentType)) { - debugLog("inject", "Skipping - unsupported subagent_type") - return - } - - // Resolve active task in this priority order (only later steps - // run when earlier ones miss): - // 1. Exact session runtime context lookup for input.sessionID - // 2. `Active task: <path>` hint in the dispatch prompt - // (explicit per-dispatch override — beats single-session - // inference so multi-window users can disambiguate) - // 3. Single-session fallback — only when exactly 1 session - // runtime file exists locally - let taskDir = null - let taskSource = null - - const contextKey = ctx.getContextKey(input) - if (contextKey) { - const context = ctx.readContext(contextKey) - const exactRef = ctx.normalizeTaskRef(context?.current_task || "") - if (exactRef) { - taskDir = exactRef - taskSource = `session:${contextKey}` - } - } - - if (!taskDir) { - const hintRef = extractActiveTaskHint(originalPrompt) - if (hintRef) { - const hintNormalized = ctx.normalizeTaskRef(hintRef) - if (hintNormalized) { - const hintDir = ctx.resolveTaskDir(hintNormalized) - if (hintDir && existsSync(hintDir)) { - taskDir = hintNormalized - taskSource = "prompt-hint" - debugLog("inject", "Resolved task from Active task: hint:", hintNormalized) - } - } - } - } - - if (!taskDir) { - const fallback = ctx._resolveSingleSessionFallback() - if (fallback?.taskPath) { - const fallbackDir = ctx.resolveTaskDir(fallback.taskPath) - if (fallbackDir && existsSync(fallbackDir)) { - taskDir = fallback.taskPath - taskSource = fallback.source - debugLog("inject", "Resolved task via single-session fallback:", taskDir, "source:", taskSource) - } - } - } - - // Agents requiring task directory - if (AGENTS_REQUIRE_TASK.includes(subagentType)) { - // subagentType is already stripped of "trellis-" prefix above - if (!taskDir) { - debugLog("inject", "Skipping - no current task") - return - } - const taskDirFull = ctx.resolveTaskDir(taskDir) - if (!taskDirFull || !existsSync(taskDirFull)) { - debugLog("inject", "Skipping - task directory not found") - return - } - } - - // Check for [finish] marker - const isFinish = originalPrompt.toLowerCase().includes("[finish]") - - // Get context based on agent type - let context = "" - switch (subagentType) { - case "implement": - context = getImplementContext(ctx, taskDir) - break - case "check": - context = isFinish - ? getFinishContext(ctx, taskDir) - : getCheckContext(ctx, taskDir) - break - case "research": - context = getResearchContext(ctx, taskDir) - break - } - - if (!context) { - debugLog("inject", "No context to inject") - return - } - - const newPrompt = buildPrompt(subagentType, originalPrompt, context, isFinish) - - // Mutate args in-place — whole-object replacement does NOT work for the task tool - // because the runtime holds a local reference to the same args object. - args.prompt = newPrompt - - debugLog("inject", "Injected context for", subagentType, "prompt length:", newPrompt.length) - - } catch (error) { - debugLog("inject", "Error in tool.execute.before:", error.message, error.stack) - } - } - } -} diff --git a/.opencode/plugins/inject-workflow-state.js b/.opencode/plugins/inject-workflow-state.js deleted file mode 100644 index 3bcade7..0000000 --- a/.opencode/plugins/inject-workflow-state.js +++ /dev/null @@ -1,162 +0,0 @@ -/* global process */ -/** - * Trellis Workflow State Injection Plugin - * - * Per-turn UserPromptSubmit equivalent for OpenCode. - * - * On every chat.message, if a Trellis task is active, inject a short - * <workflow-state> breadcrumb reminding the main AI what task is - * active and its expected flow. Breadcrumb text is pulled exclusively - * from the project's workflow.md [workflow-state:STATUS] tag blocks — - * workflow.md is the single source of truth. There are no fallback - * tables in this plugin: when workflow.md is missing or a tag is - * absent, the breadcrumb degrades to a generic - * "Refer to workflow.md for current step." line so users see (and fix) - * the broken state instead of the plugin silently masking it. - * - * Unlike session-start, this plugin does NOT dedupe — the breadcrumb - * should surface on every turn so long conversations don't drift. - * - * Silently skips when: - * - No .trellis/ directory - * - No active task in the session runtime context - * - task.json malformed or missing status - */ - -import { existsSync, readFileSync } from "fs" -import { join } from "path" -import { TrellisContext, debugLog, isTrellisSubagent } from "../lib/trellis-context.js" - -// Supports STATUS values with letters, digits, underscores, hyphens -// (so "in-review" / "blocked-by-team" work alongside "in_progress"). -const TAG_RE = /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g - -/** - * Parse workflow.md for [workflow-state:STATUS] blocks. - * - * Returns {status: body}. workflow.md is the single source of truth — - * there are no fallback tables here. Missing tags (or a missing / - * unreadable workflow.md) fall back to a generic line in - * buildBreadcrumb so users see the broken state and fix workflow.md - * rather than the plugin silently masking it. - */ -function loadBreadcrumbs(directory) { - const workflowPath = join(directory, ".trellis", "workflow.md") - if (!existsSync(workflowPath)) return {} - let content - try { - content = readFileSync(workflowPath, "utf-8") - } catch { - return {} - } - const result = {} - for (const match of content.matchAll(TAG_RE)) { - const status = match[1] - const body = match[2].trim() - if (body) result[status] = body - } - return result -} - -/** - * Get (taskId, status) from active task, or null if no active task. - */ -function getActiveTask(ctx, platformInput = null) { - const active = ctx.getActiveTask(platformInput) - const taskRef = active.taskPath - if (!taskRef) return null - const taskDir = ctx.resolveTaskDir(taskRef) - if (active.stale || !taskDir || !existsSync(taskDir)) { - return { id: taskRef.split("/").pop(), status: "stale", source: active.source } - } - const taskJsonPath = join(taskDir, "task.json") - if (!existsSync(taskJsonPath)) return null - try { - const data = JSON.parse(readFileSync(taskJsonPath, "utf-8")) - const status = typeof data.status === "string" ? data.status : "" - if (!status) return null - const id = data.id || taskRef.split("/").pop() - return { id, status, source: active.source } - } catch { - return null - } -} - -/** - * Build the <workflow-state>...</workflow-state> block. - * - Known status (tag present in workflow.md) → detailed body - * - Unknown status (no tag, or workflow.md missing) → generic - * "Refer to workflow.md for current step." line - * - no_task pseudo-status (id === null) → header omits task info - */ -function buildBreadcrumb(id, status, templates, source = null) { - let body = templates[status] - if (body === undefined) { - body = "Refer to workflow.md for current step." - } - let header = id === null ? `Status: ${status}` : `Task: ${id} (${status})` - if (source) { - header = `${header}\nSource: ${source}` - } - return `<workflow-state>\n${header}\n${body}\n</workflow-state>` -} - -// OpenCode 1.2.x expects plugins to be factory functions (see inject-subagent-context.js comment). -export default async ({ directory }) => { - const ctx = new TrellisContext(directory) - debugLog("workflow-state", "Plugin loaded, directory:", directory) - - return { - // chat.message fires on every user message. Inject breadcrumb in-place - // so it persists in conversation history. - "chat.message": async (input, output) => { - try { - // Skip Trellis sub-agent turns — the per-turn breadcrumb is for the - // main session only; sub-agent context comes from the parent's - // tool.execute.before injection. - if (isTrellisSubagent(input)) { - debugLog("workflow-state", "Skipping trellis subagent turn:", input?.agent) - return - } - if (process.env.TRELLIS_HOOKS === "0" || process.env.TRELLIS_DISABLE_HOOKS === "1") { - return - } - if (process.env.OPENCODE_NON_INTERACTIVE === "1") { - return - } - if (!ctx.isTrellisProject()) { - return - } - const templates = loadBreadcrumbs(directory) - const task = getActiveTask(ctx, input) - const breadcrumb = task - ? buildBreadcrumb(task.id, task.status, templates, task.source) - : buildBreadcrumb(null, "no_task", templates) - - const parts = output?.parts || [] - const textPartIndex = parts.findIndex( - p => p.type === "text" && p.text !== undefined, - ) - if (textPartIndex !== -1) { - const originalText = parts[textPartIndex].text || "" - parts[textPartIndex].text = `${breadcrumb}\n\n${originalText}` - } else { - parts.unshift({ type: "text", text: breadcrumb }) - } - debugLog( - "workflow-state", - "Injected breadcrumb for task", - task ? task.id : "none", - "status", - task ? task.status : "no_task", - ) - } catch (error) { - debugLog( - "workflow-state", - "Error in chat.message:", - error instanceof Error ? error.message : String(error), - ) - } - }, - } -} diff --git a/.opencode/plugins/session-start.js b/.opencode/plugins/session-start.js deleted file mode 100644 index ee1508e..0000000 --- a/.opencode/plugins/session-start.js +++ /dev/null @@ -1,101 +0,0 @@ -/* global process */ -/** - * Trellis Session Start Plugin - * - * Injects context when user sends the first message in a session. - * Uses OpenCode's chat.message hook directly so the context persists in history. - */ - -import { TrellisContext, contextCollector, debugLog, isTrellisSubagent } from "../lib/trellis-context.js" -import { - buildSessionContext, - hasPersistedInjectedContext, - markContextInjected, -} from "../lib/session-utils.js" - -// OpenCode 1.2.x expects plugins to be factory functions (see inject-subagent-context.js comment). -export default async ({ directory, client }) => { - const ctx = new TrellisContext(directory) - debugLog("session", "Plugin loaded, directory:", directory) - - return { - event: ({ event }) => { - try { - if (event?.type === "session.compacted" && event?.properties?.sessionID) { - const sessionID = event.properties.sessionID - contextCollector.clear(sessionID) - debugLog("session", "Cleared processed flag after compaction for session:", sessionID) - } - } catch (error) { - debugLog( - "session", - "Error in event hook:", - error instanceof Error ? error.message : String(error), - ) - } - }, - - // chat.message - triggered when user sends a message. - // Modify the message in-place so the context is persisted with updateMessage/updatePart. - "chat.message": async (input, output) => { - try { - const sessionID = input.sessionID - const agent = input.agent || "unknown" - debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent) - - // Skip Trellis sub-agent turns — sub-agent context is injected by - // `inject-subagent-context.js` on the parent's tool.execute.before; - // re-injecting the main-session SessionStart here would drown that. - if (isTrellisSubagent(input)) { - debugLog("session", "Skipping trellis subagent turn:", agent) - return - } - - if (process.env.TRELLIS_HOOKS === "0" || process.env.TRELLIS_DISABLE_HOOKS === "1") { - debugLog("session", "Skipping - TRELLIS_HOOKS disabled") - return - } - - if (process.env.OPENCODE_NON_INTERACTIVE === "1") { - debugLog("session", "Skipping - non-interactive mode") - return - } - - if (contextCollector.isProcessed(sessionID)) { - debugLog("session", "Skipping - session already processed") - return - } - - if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) { - contextCollector.markProcessed(sessionID) - debugLog("session", "Skipping - session already contains persisted Trellis context") - return - } - - const context = buildSessionContext(ctx, input) - debugLog("session", "Built context, length:", context.length) - - const parts = output?.parts || [] - const textPartIndex = parts.findIndex( - p => p.type === "text" && p.text !== undefined - ) - - if (textPartIndex !== -1) { - const originalText = parts[textPartIndex].text || "" - parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}` - markContextInjected(parts[textPartIndex]) - debugLog("session", "Injected context into chat.message text part, length:", context.length) - } else { - const injectedPart = { type: "text", text: context } - markContextInjected(injectedPart) - parts.unshift(injectedPart) - debugLog("session", "Prepended new text part with context, length:", context.length) - } - - contextCollector.markProcessed(sessionID) - } catch (error) { - debugLog("session", "Error in chat.message:", error.message, error.stack) - } - }, - } -} diff --git a/.opencode/skills/trellis-before-dev/SKILL.md b/.opencode/skills/trellis-before-dev/SKILL.md deleted file mode 100644 index 9c6ec9c..0000000 --- a/.opencode/skills/trellis-before-dev/SKILL.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -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/<package>/<layer>/index.md - ``` - Follow the **"Pre-Development Checklist"** section in the index. - -4. **Read the specific guideline files** listed in the Pre-Development Checklist that are relevant to your task. The index is NOT the goal — it points you to the actual guideline files (e.g., `error-handling.md`, `conventions.md`, `mock-strategies.md`). Read those files to understand the coding standards and patterns. - -5. **Always read shared guides**: - ```bash - cat .trellis/spec/guides/index.md - ``` - -6. Understand the coding standards and patterns you need to follow, then proceed with your development plan. - -This step is **mandatory** before writing any code. diff --git a/.opencode/skills/trellis-brainstorm/SKILL.md b/.opencode/skills/trellis-brainstorm/SKILL.md deleted file mode 100644 index 670e19b..0000000 --- a/.opencode/skills/trellis-brainstorm/SKILL.md +++ /dev/null @@ -1,548 +0,0 @@ ---- -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: <short goal>" --slug <auto>) -``` - -Use a slug without a date prefix. `task.py create` adds the `MM-DD-` -directory prefix automatically. - -Create/seed `prd.md` immediately with what you know: - -```markdown -# brainstorm: <short goal> - -## Goal - -<one paragraph: what + why> - -## What I already know - -* <facts from user message> -* <facts discovered from repo/docs> - -## Assumptions (temporary) - -* <assumptions to validate> - -## Open Questions - -* <ONLY Blocking / Preference questions; keep list short> - -## Requirements (evolving) - -* <start with what is known> - -## Acceptance Criteria (evolving) - -* [ ] <testable criterion> - -## Definition of Done (team quality bar) - -* Tests added/updated (unit/integration where appropriate) -* Lint / typecheck / CI green -* Docs/notes updated if behavior changes -* Rollout/rollback considered if risky - -## Out of Scope (explicit) - -* <what we will not do in this task> - -## Technical Notes - -* <files inspected, constraints, links, references> -* <research notes summary if applicable> -``` - ---- - -## Step 1: Auto-Context (DO THIS BEFORE ASKING QUESTIONS) - -Before asking questions like "what does the code look like?", gather context yourself: - -### Repo inspection checklist - -* Identify likely modules/files impacted -* Locate existing patterns (similar features, conventions, error handling style) -* Check configs, scripts, existing command definitions -* Note any constraints (runtime, dependency policy, build tooling) - -### Documentation checklist - -* Look for existing PRDs/specs/templates -* Look for command usage examples, README, ADRs if any - -Write findings into PRD: - -* Add to `What I already know` -* Add constraints/links to `Technical Notes` - ---- - -## Step 2: Classify Complexity (still useful, not gating task creation) - -| Complexity | Criteria | Action | -| ------------ | ------------------------------------------------------ | ------------------------------------------- | -| **Trivial** | Single-line fix, typo, obvious change | Skip brainstorm, implement directly | -| **Simple** | Clear goal, 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/<topic>.md` (the contract — see `workflow.md` Phase 1.2) -- It returns only `{file path, one-line summary}` to the main agent -- Independent topics can be **parallelized** — spawn multiple sub-agents in one tool call - -> **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/<topic>.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 <specific question>; persist findings to `{TASK_DIR}/research/<topic-slug>.md`." - -❌ Bad (what you must NOT do): -``` -Main agent: WebFetch(url-A) → WebFetch(url-B) → Bash(gh api ...) - → WebSearch(q1) → WebSearch(q2) → ... (10+ inline calls) - → Write(research/topic.md) -``` -→ Pollutes main context with raw HTML/JSON, burns tokens. - -✅ Good: -``` -Main agent: Task(subagent_type="trellis-research", - prompt="Research topic A; persist to research/topic-a.md") - + Task(subagent_type="trellis-research", - prompt="Research topic B; persist to research/topic-b.md") - + Task(subagent_type="trellis-research", - prompt="Research topic C; persist to research/topic-c.md") -→ Reads research/topic-{a,b,c}.md after they finish. -``` - -### Research steps (to pass into each sub-agent prompt) - -Each `trellis-research` sub-agent should: - -1. Identify 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/<topic>.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/<topic-a>.md`](research/<topic-a>.md) — <one-line takeaway> -* [`research/<topic-b>.md`](research/<topic-b>.md) — <one-line takeaway> - -## Research Notes - -### What similar tools do - -* ... -* ... - -### Constraints from our repo/project - -* ... - -### Feasible approaches here - -**Approach A: <name>** (Recommended) - -* How it works: -* Pros: -* Cons: - -**Approach B: <name>** - -* How it works: -* Pros: -* Cons: - -**Approach C: <name>** (optional) - -* ... -``` - -Then ask **one** preference question: - -* "Which approach do you prefer: A / B / C (or other)?" - ---- - -## Step 5: Expansion Sweep (DIVERGE) — Required after initial understanding - -After you can summarize the goal, proactively broaden thinking before converging. - -### Expansion categories (keep to 1–2 bullets each) - -1. **Future evolution** - - * What might this feature become in 1–3 months? - * What extension points are worth preserving now? - -2. **Related scenarios** - - * What adjacent commands/flows should remain consistent with this? - * Are there parity expectations (create vs update, import vs export, etc.)? - -3. **Failure & edge cases** - - * Conflicts, offline/network failure, retries, idempotency, compatibility, rollback - * Input validation, security boundaries, permission checks - -### Expansion message template (to user) - -```markdown -I understand you want to implement: <current goal>. - -Before diving into design, let me quickly diverge to consider three categories (to avoid rework later): - -1. Future evolution: <1–2 bullets> -2. Related scenarios: <1–2 bullets> -3. Failure/edge cases: <1–2 bullets> - -For this MVP, which would you like to include (or none)? - -1. Current requirement only (minimal viable) -2. Add <X> (reserve for future extension) -3. Add <Y> (improve robustness/consistency) -4. Other: describe your preference -``` - -Then update PRD: - -* What's in MVP → `Requirements` -* What's excluded → `Out of Scope` - ---- - -## Step 6: Q&A Loop (CONVERGE) - -### Rules - -* One question per message -* Prefer multiple-choice when possible -* After each user answer: - - * Update PRD immediately - * Move answered items from `Open Questions` → `Requirements` - * Update `Acceptance Criteria` with testable checkboxes - * Clarify `Out of Scope` - -### Question priority (recommended) - -1. **MVP scope boundary** (what is included/excluded) -2. **Preference decisions** (after presenting concrete options) -3. **Failure/edge behavior** (only for MVP-critical paths) -4. **Success metrics & Acceptance Criteria** (what proves it works) - -### Preferred question format (multiple choice) - -```markdown -For <topic>, which approach do you prefer? - -1. **Option A** — <what it means + trade-off> -2. **Option B** — <what it means + trade-off> -3. **Option C** — <what it means + trade-off> -4. **Other** — describe your preference -``` - ---- - -## Step 7: Propose Approaches + Record Decisions (Complex tasks) - -After requirements are clear enough, propose 2–3 approaches (if not already done via research-first): - -```markdown -Based on current information, here are 2–3 feasible approaches: - -**Approach A: <name>** (Recommended) - -* How: -* Pros: -* Cons: - -**Approach B: <name>** - -* How: -* Pros: -* Cons: - -Which direction do you prefer? -``` - -Record the outcome in PRD as an ADR-lite section: - -```markdown -## Decision (ADR-lite) - -**Context**: Why this decision was needed -**Decision**: Which approach was chosen -**Consequences**: Trade-offs, risks, potential future improvements -``` - ---- - -## Step 8: Final Confirmation + Implementation Plan - -When open questions are resolved, confirm complete requirements with a structured summary: - -### Final confirmation format - -```markdown -Here's my understanding of the complete requirements: - -**Goal**: <one sentence> - -**Requirements**: - -* ... -* ... - -**Acceptance Criteria**: - -* [ ] ... -* [ ] ... - -**Definition of Done**: - -* ... - -**Out of Scope**: - -* ... - -**Technical Approach**: -<brief summary + key decisions> - -**Implementation Plan (small PRs)**: - -* PR1: <scaffolding + tests + minimal plumbing> -* PR2: <core behavior> -* PR3: <edge cases + docs + cleanup> - -Does this look correct? If yes, I'll proceed with implementation. -``` - -### Subtask Decomposition (Complex Tasks) - -For complex tasks with multiple independent work items, create subtasks: - -```bash -# Create child tasks -CHILD1=$(python3 ./.trellis/scripts/task.py create "Child task 1" --slug child1 --parent "$TASK_DIR") -CHILD2=$(python3 ./.trellis/scripts/task.py create "Child task 2" --slug child2 --parent "$TASK_DIR") - -# Or link existing tasks -python3 ./.trellis/scripts/task.py add-subtask "$TASK_DIR" "$CHILD_DIR" -``` - ---- - -## PRD Target Structure (final) - -`prd.md` should converge to: - -```markdown -# <Task Title> - -## Goal - -<why + what> - -## Requirements - -* ... - -## Acceptance Criteria - -* [ ] ... - -## Definition of Done - -* ... - -## Technical Approach - -<key design + decisions> - -## Decision (ADR-lite) - -Context / Decision / Consequences - -## Out of Scope - -* ... - -## Technical Notes - -<constraints, references, files, research notes> -``` - ---- - -## Anti-Patterns (Hard Avoid) - -* Asking user for code/context that can be derived from repo -* Asking user to choose an approach before presenting concrete options -* Meta questions about whether to research -* Staying narrowly on the initial request without considering evolution/edges -* Letting brainstorming drift without updating PRD - ---- - -## Integration with Start Workflow - -After brainstorm completes (Step 8 confirmation approved), the flow continues to the Task Workflow's **Phase 2: Prepare for Implementation**: - -```text -Brainstorm - Step 0: Create task directory + seed PRD - Step 1–7: Discover requirements, research, converge - Step 8: Final confirmation → user approves - ↓ -Task Workflow Phase 2 (Prepare for Implementation) - Code-Spec Depth Check (if applicable) - → Research codebase (based on confirmed PRD) - → Configure code-spec context (jsonl files) - → Activate task - ↓ -Task Workflow Phase 3 (Execute) - Implement → Check → Complete -``` - -The task directory and PRD already exist from brainstorm, so Phase 1 of the Task Workflow is skipped entirely. - ---- - -## Related Commands - -| Command | When to Use | -|---------|-------------| -| `/trellis:start` | Entry point that triggers brainstorm | -| `/trellis:finish-work` | After implementation is complete | -| `/trellis:update-spec` | If new patterns emerge during work | diff --git a/.opencode/skills/trellis-break-loop/SKILL.md b/.opencode/skills/trellis-break-loop/SKILL.md deleted file mode 100644 index ef2b50c..0000000 --- a/.opencode/skills/trellis-break-loop/SKILL.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -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/.opencode/skills/trellis-check/SKILL.md b/.opencode/skills/trellis-check/SKILL.md deleted file mode 100644 index 16b3dc4..0000000 --- a/.opencode/skills/trellis-check/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -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/<package>/<layer>/index.md -``` - -Read the specific guideline files referenced — the index is a pointer, not the goal. - -## Step 3: Run Project Checks - -Run the project's lint, type-check, and test commands. Fix any failures before proceeding. - -## Step 4: Review Against Checklist - -### Code Quality - -- [ ] Linter passes? -- [ ] Type checker passes (if applicable)? -- [ ] Tests pass? -- [ ] No debug logging left in? -- [ ] No suppressed warnings or type-safety bypasses? - -### Test Coverage - -- [ ] New function → unit test added? -- [ ] Bug fix → regression test added? -- [ ] Changed behavior → existing tests updated? - -### Spec Sync - -- [ ] Does `.trellis/spec/` need updates? (new patterns, conventions, lessons learned) - -> "If I fixed a bug or discovered something non-obvious, should I document it so future me won't hit the same issue?" → If YES, update the relevant spec doc. - -## Step 5: Cross-Layer Dimensions (if applicable) - -Skip this step if your change is confined to a single layer. - -### A. Data Flow (changes touch 3+ layers) - -- [ ] Read flow traces correctly: Storage → Service → API → UI -- [ ] Write flow traces correctly: UI → API → Service → Storage -- [ ] Types/schemas correctly passed between layers? -- [ ] Errors properly propagated to caller? - -### B. Code Reuse (modifying constants, creating utilities) - -- [ ] Searched for existing similar code before creating new? - ```bash - grep -r "pattern" src/ - ``` -- [ ] If 2+ places define same value → extracted to shared constant? -- [ ] After batch modification, all occurrences updated? - -### C. Import/Dependency (creating new files) - -- [ ] Correct import paths (relative vs absolute)? -- [ ] No circular dependencies? - -### D. Same-Layer Consistency - -- [ ] Other places using the same concept are consistent? - ---- - -## Step 6: Report and Fix - -Report violations found and fix them directly. Re-run project checks after fixes. diff --git a/.opencode/skills/trellis-meta/SKILL.md b/.opencode/skills/trellis-meta/SKILL.md deleted file mode 100644 index 590bfac..0000000 --- a/.opencode/skills/trellis-meta/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -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/.opencode/skills/trellis-meta/references/customize-local/add-project-local-conventions.md b/.opencode/skills/trellis-meta/references/customize-local/add-project-local-conventions.md deleted file mode 100644 index 608aaa6..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/add-project-local-conventions.md +++ /dev/null @@ -1,83 +0,0 @@ -# Add Project-Local Conventions - -Often the user does not need to change Trellis mechanics; they need local AI to understand their team's conventions. In that case, prefer `.trellis/spec/` or a project-local skill instead of editing `trellis-meta`. - -## Where To Put Things - -| Content type | Location | -| --- | --- | -| Rules code must follow | `.trellis/spec/<layer>/` | -| Cross-layer thinking methods | `.trellis/spec/guides/` | -| AI capability for a project-specific flow | Platform-local skill | -| One-off task material | `.trellis/tasks/<task>/` | -| Session summary | `.trellis/workspace/<developer>/journal-N.md` | - -## Create A Project-Local Skill - -If the user wants AI to know "how this project customizes Trellis," create a local skill: - -```text -.claude/skills/trellis-local/ -└── SKILL.md -``` - -Example: - -```md ---- -name: trellis-local -description: "Project-local Trellis customizations for this repository. Use when changing this project's Trellis workflow, hooks, local agents, or team-specific conventions." ---- - -# Trellis Local - -## Local Scope - -This skill documents this repository's Trellis customizations only. - -## Custom Workflow Rules - -- ... - -## Local Hook Changes - -- ... - -## Local Agent Changes - -- ... -``` - -For multi-platform projects, place equivalent versions in other platform skill directories, or use `.agents/skills/` for platforms that support the shared layer. - -## Write To `.trellis/spec/` - -If the content is a coding convention, write it to spec. Examples: - -```text -.trellis/spec/backend/error-handling.md -.trellis/spec/frontend/components.md -.trellis/spec/guides/cross-platform-thinking-guide.md -``` - -After writing it, update the corresponding `index.md` so AI can find the new rule from the entry point. - -## Make The Current Task Use New Conventions - -After writing a spec, add it to the current task context: - -```bash -python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/backend/error-handling.md" "Error handling conventions" -python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/backend/error-handling.md" "Review error handling" -``` - -## Do Not Store Project-Private Rules In `trellis-meta` - -`trellis-meta` is a public skill for understanding Trellis architecture and local customization entry points. Put project-private content in: - -- `.trellis/spec/` -- a project-local skill -- the current task -- workspace journal - -This prevents future updates to Trellis's built-in `trellis-meta` from overwriting the team's own conventions. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-agents.md b/.opencode/skills/trellis-meta/references/customize-local/change-agents.md deleted file mode 100644 index 9b63531..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-agents.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/customize-local/change-context-loading.md b/.opencode/skills/trellis-meta/references/customize-local/change-context-loading.md deleted file mode 100644 index 556b4e3..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-context-loading.md +++ /dev/null @@ -1,81 +0,0 @@ -# Change Local Context Loading - -Context loading determines when AI reads workflow, task, spec, research, workspace, and git status. Read this page when the user says "AI does not know the current task," "the agent did not read specs," or "there is too much/too little context." - -## Read These Files First - -1. `.trellis/workflow.md` -2. `.trellis/scripts/get_context.py` -3. `.trellis/scripts/common/session_context.py` -4. `.trellis/scripts/common/task_context.py` -5. `.trellis/scripts/common/active_task.py` -6. Current platform hooks or agent files -7. The current task's `implement.jsonl` / `check.jsonl` - -## Context Sources - -| Source | Purpose | -| --- | --- | -| `.trellis/workflow.md` | Workflow and next-action hints. | -| `.trellis/tasks/<task>/prd.md` | Current task requirements. | -| `.trellis/tasks/<task>/implement.jsonl` | Spec/research to read before implementation. | -| `.trellis/tasks/<task>/check.jsonl` | Spec/research to read during checking. | -| `.trellis/spec/` | Project specs. | -| `.trellis/workspace/` | Session records. | -| git status | Current working tree changes. | - -## Common Needs And Edit Points - -| Need | Edit point | -| --- | --- | -| Inject more/less information in new sessions | `session_context.py` or the platform `session-start` hook. | -| Change hints on each user input | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The `inject-workflow-state` hook is parser-only and reads the block verbatim. | -| Agent did not read specs | Task JSONL, agent prelude, `inject-subagent-context` hook. | -| Active task is lost | `active_task.py` and platform session identity propagation. | -| Change JSONL validation rules | `task_context.py`. | - -## JSONL Rules - -`implement.jsonl` / `check.jsonl` are the key context loading interface: - -```jsonl -{"file": ".trellis/spec/backend/index.md", "reason": "Backend conventions"} -{"file": ".trellis/tasks/04-28-x/research/api.md", "reason": "API research"} -``` - -Include only spec/research files. Do not put code files that will be modified into these manifests; agents read code files themselves during implementation. - -## Change Session Context - -If the user wants every new session to see more project state, edit: - -- `.trellis/scripts/common/session_context.py` -- the corresponding platform `session-start` hook - -Context cannot grow without bound. Prefer injecting indexes and paths so the AI can read detailed files on demand. - -## Change Sub-Agent Context - -First determine which mode the platform uses: - -- hook push: edit the `inject-subagent-context` hook. -- agent pull: edit the read steps in the corresponding `trellis-implement` / `trellis-check` agent file. - -In both modes, make sure the agent ultimately reads: - -1. active task -2. `prd.md` -3. `info.md` if present -4. the corresponding JSONL -5. spec/research referenced by the JSONL - -## Troubleshooting Order - -```bash -python3 ./.trellis/scripts/task.py current --source -python3 ./.trellis/scripts/task.py list-context <task> -python3 ./.trellis/scripts/task.py validate <task> -python3 ./.trellis/scripts/get_context.py --mode packages -``` - -Confirm the task and JSONL are correct before editing hooks/agents. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-hooks.md b/.opencode/skills/trellis-meta/references/customize-local/change-hooks.md deleted file mode 100644 index 5c1ed7a..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-hooks.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 <task> -``` - -If the task and JSONL are correct, determine whether the platform uses hook push or agent pull. For hook push, edit `inject-subagent-context`; for agent pull, edit the agent file. - -## Notes - -- Settings handle registration, hook scripts handle behavior; inspect both together. -- Different platforms support different hook events. Do not directly copy another platform's settings. -- Hooks should read project-local `.trellis/`; they should not depend on Trellis upstream source paths. -- Hook failures should produce visible errors so AI does not silently lose context. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-skills-or-commands.md b/.opencode/skills/trellis-meta/references/customize-local/change-skills-or-commands.md deleted file mode 100644 index 84590a1..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-skills-or-commands.md +++ /dev/null @@ -1,78 +0,0 @@ -# Change Local Skills, Commands, Prompts, And Workflows - -When the user wants to change AI entry points, auto-trigger rules, or explicit command behavior, edit skills, commands, prompts, or workflows in local platform directories. - -## Read These Files First - -1. `.trellis/workflow.md` -2. Target platform skill/command/prompt/workflow directory -3. Related agent or hook files -4. Whether project rules already exist in `.trellis/spec/` - -## Which Entry Type To Choose - -| Goal | Recommendation | -| --- | --- | -| AI should automatically know a capability | Add or modify a skill. | -| User wants to trigger manually with a command | Add or modify a command/prompt/workflow. | -| Team project conventions | Prefer `.trellis/spec/` or a project-local skill. | -| Change Trellis flow semantics | Synchronize `.trellis/workflow.md`. | - -## Modify A Skill - -A skill is usually: - -```text -<skill-name>/ -├── SKILL.md -└── references/ -``` - -`SKILL.md` should be short and responsible for triggering/routing. Put long content in `references/` so AI can read it on demand. - -The frontmatter description should specify when to use the skill. Example: - -```yaml -description: "Use when customizing this project's deployment workflow and release checklist." -``` - -Do not write vague descriptions such as "helpful project skill"; they can trigger incorrectly. - -## Modify A Command/Prompt/Workflow - -Explicit entry points should state: - -- How the user triggers it. -- Which `.trellis/` files to read. -- Which scripts to run. -- How to report after completion. - -If a command only repeats workflow rules, prefer making it reference/read `.trellis/workflow.md` instead of maintaining a second copy of the flow. - -## Common Paths - -| Platform | Entry directories | -| --- | --- | -| Claude Code | `.claude/skills/`, `.claude/commands/` | -| Cursor | `.cursor/skills/`, `.cursor/commands/` | -| OpenCode | `.opencode/skills/`, `.opencode/commands/` | -| Codex | `.agents/skills/`, `.codex/skills/` | -| GitHub Copilot | `.github/skills/`, `.github/prompts/` | -| Kilo / Antigravity / Windsurf | workflows + skills | - -## Add A Project-Local Skill - -If the user wants to document team-private customizations, create a project-local skill, for example: - -```text -.claude/skills/project-trellis-local/ -└── SKILL.md -``` - -For multi-platform projects, add equivalent versions in each platform skill directory, or use `.agents/skills/` on platforms that support the shared layer. - -## Notes - -- Do not mix every platform's syntax into one file. -- Do not change only one platform entry point while claiming all platforms are supported. -- Do not hide long-term engineering conventions inside a command; write them to `.trellis/spec/`. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-spec-structure.md b/.opencode/skills/trellis-meta/references/customize-local/change-spec-structure.md deleted file mode 100644 index 358de51..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-spec-structure.md +++ /dev/null @@ -1,83 +0,0 @@ -# Change Local Spec Structure - -When the user wants to change the engineering conventions AI follows, add new spec layers, or adjust monorepo package mapping, edit `.trellis/spec/` and `.trellis/config.yaml`. - -## Read These Files First - -1. `.trellis/config.yaml` -2. `.trellis/spec/` -3. `.trellis/workflow.md` Phase 1.3 and Phase 3.3 -4. Current task `implement.jsonl` / `check.jsonl` - -## Common Needs - -| Need | Edit location | -| --- | --- | -| Add backend/frontend/docs/test spec layer | `.trellis/spec/<layer>/` or `.trellis/spec/<package>/<layer>/` | -| Add shared thinking guides | `.trellis/spec/guides/` | -| Adjust monorepo packages | `packages` in `.trellis/config.yaml` | -| Change default package | `default_package` in `.trellis/config.yaml` | -| Control spec scanning scope | `spec_scope` in `.trellis/config.yaml` | -| Make a task read a new spec | Task `implement.jsonl` / `check.jsonl` | - -## Add A Spec Layer - -Single-repository example: - -```text -.trellis/spec/security/ -├── index.md -└── auth.md -``` - -Monorepo example: - -```text -.trellis/spec/webapp/security/ -├── index.md -└── auth.md -``` - -`index.md` should include: - -- What code this layer applies to. -- Pre-Development Checklist. -- Quality Check. -- Links to specific guideline files. - -## Update Context - -Adding a spec does not mean every task automatically reads it. The current task must reference it in JSONL: - -```bash -python3 ./.trellis/scripts/task.py add-context <task> implement ".trellis/spec/webapp/security/index.md" "Security conventions" -python3 ./.trellis/scripts/task.py add-context <task> check ".trellis/spec/webapp/security/index.md" "Security review rules" -``` - -## Change Monorepo Packages - -Example `.trellis/config.yaml`: - -```yaml -packages: - webapp: - path: apps/web - api: - path: apps/api -default_package: webapp -``` - -After editing, run: - -```bash -python3 ./.trellis/scripts/get_context.py --mode packages -``` - -Use this output to confirm AI can see the correct packages and spec layers. - -## Notes - -- Specs are user project conventions and can be changed according to project needs. -- Do not put temporary task information into specs; put temporary information in the task. -- Do not put long-term conventions only in agents or commands; preserve them in specs. -- After changing spec structure, check whether existing task JSONL files still point to files that exist. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-task-lifecycle.md b/.opencode/skills/trellis-meta/references/customize-local/change-task-lifecycle.md deleted file mode 100644 index a7a340f..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-task-lifecycle.md +++ /dev/null @@ -1,90 +0,0 @@ -# Change Local Task Lifecycle - -Task lifecycle includes creation, start, context configuration, finish, archive, parent/child tasks, and lifecycle hooks. The default customization targets are `.trellis/tasks/`, `.trellis/config.yaml`, and `.trellis/scripts/`. - -## Read These Files First - -1. `.trellis/workflow.md` -2. `.trellis/config.yaml` -3. `.trellis/scripts/task.py` -4. `.trellis/scripts/common/task_store.py` -5. `.trellis/scripts/common/task_utils.py` -6. The current task's `.trellis/tasks/<task>/task.json` - -## Common Needs And Edit Points - -| Need | Edit point | -| --- | --- | -| Automatically sync an external system after task creation | `hooks.after_create` in `.trellis/config.yaml`. | -| Automatically update status after task start | `hooks.after_start` in `.trellis/config.yaml`. | -| Run a script after task finish | `hooks.after_finish` in `.trellis/config.yaml`. | -| Clean external resources after archive | `hooks.after_archive` in `.trellis/config.yaml`. | -| Change default task fields | `.trellis/scripts/common/task_store.py`. | -| Change task parsing/search | `.trellis/scripts/common/task_utils.py`. | -| Change active task behavior | `.trellis/scripts/common/active_task.py`. | - -## lifecycle hooks - -`.trellis/config.yaml` supports: - -```yaml -hooks: - after_create: - - "python3 .trellis/scripts/hooks/my_sync.py create" - after_start: - - "python3 .trellis/scripts/hooks/my_sync.py start" - after_finish: - - "python3 .trellis/scripts/hooks/my_sync.py finish" - after_archive: - - "python3 .trellis/scripts/hooks/my_sync.py archive" -``` - -Hook commands receive the `TASK_JSON_PATH` environment variable, pointing to the current task's `task.json`. Hook failures should usually warn, but not block the main task operation. - -## Change Task Fields - -If the user wants to add project-local fields, prefer putting them under `meta` in `task.json` to avoid breaking existing scripts' assumptions about standard fields. - -Example: - -```json -"meta": { - "linearIssue": "ENG-123", - "risk": "high" -} -``` - -If standard fields really need to change, inspect every local script that reads `task.json`. - -## Change Active Task - -Active task is session-level state stored in `.trellis/.runtime/sessions/`. Do not fall back to a global `.current-task` model. If the user wants to change active task behavior, edit: - -- `.trellis/scripts/common/active_task.py` -- platform hooks or shell session bridges -- active task descriptions in `.trellis/workflow.md` - -### `task.py create` Sets the Active Pointer - -`cmd_create` in `.trellis/scripts/common/task_store.py` calls `set_active_task` best-effort right after writing the new task directory. The behavior: - -- When the calling shell carries session identity (`TRELLIS_CONTEXT_ID` env var, or any platform-specific session env that `resolve_context_key` recognizes — see `active_task.py:_ENV_SESSION_KEYS`), the per-session pointer at `.trellis/.runtime/sessions/<context_key>.json` is rewritten to point at the new task. The task's `status=planning` and `[workflow-state:planning]` fires on the very next `UserPromptSubmit`. -- When session identity is unavailable (raw CLI invocation outside an AI session, or a platform that doesn't propagate identity to shell), the task directory is still created and `status=planning` is still written, but the active pointer is left untouched. The user can attach the task later with `task.py start <dir>` once they're back in an AI session. - -This makes `[workflow-state:planning]` the live breadcrumb during the brainstorm and JSONL curation work that follows `task.py create`. The pre-R7 behavior left the breadcrumb stuck on `no_task` until `task.py start`, so the planning block was effectively dead text. - -If you fork `task.py` to add a new creation path (e.g. an external import that bypasses `cmd_create`), audit whether your path also calls `set_active_task`. Without that call, your created tasks will not surface as active. The full status writer table is in `.trellis/spec/cli/backend/workflow-state-contract.md`. - -## Modification Steps - -1. Confirm the current task with `python3 ./.trellis/scripts/task.py current --source`. -2. Read the current task's `task.json` and confirm status and fields. -3. For configuration needs, edit `.trellis/config.yaml` first. -4. For script behavior needs, then edit `.trellis/scripts/`. -5. If the AI flow changed, synchronize `.trellis/workflow.md`. - -## Do Not - -- Do not directly edit `.trellis/.runtime/sessions/` to "fix" business state. -- Do not hard-code project-private fields into scripts; prefer `meta`. -- Do not default to asking the user to fork Trellis CLI. diff --git a/.opencode/skills/trellis-meta/references/customize-local/change-workflow.md b/.opencode/skills/trellis-meta/references/customize-local/change-workflow.md deleted file mode 100644 index 4231845..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/change-workflow.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/customize-local/overview.md b/.opencode/skills/trellis-meta/references/customize-local/overview.md deleted file mode 100644 index b53b090..0000000 --- a/.opencode/skills/trellis-meta/references/customize-local/overview.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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/<task>/` | -| Project specs | `.trellis/spec/` | -| Runtime scripts | `.trellis/scripts/` | -| Platform integration | `.claude/`, `.codex/`, `.cursor/`, `.opencode/`, and similar directories | -| Shared skill | `.agents/skills/` | - -## Things Not To Do By Default - -- Do not edit the global npm install directory. -- Do not edit `node_modules/@mindfoldhq/trellis`. -- Do not assume the user has the Trellis GitHub repository. -- Do not overwrite local files already modified by the user with default templates. -- Do not put team project rules into public `trellis-meta`; project rules belong in `.trellis/spec/` or a local skill. - -## When To Inspect Upstream Source - -Switch to an upstream source-code perspective only when the user explicitly expresses one of these goals: - -- "I want to open a PR to Trellis" -- "I want to change npm package publish contents" -- "I want to fork Trellis" -- "I want to modify the generation logic for `trellis init/update`" - -Otherwise, default to modifying local Trellis files inside the user project. diff --git a/.opencode/skills/trellis-meta/references/local-architecture/context-injection.md b/.opencode/skills/trellis-meta/references/local-architecture/context-injection.md deleted file mode 100644 index fae6fa5..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/context-injection.md +++ /dev/null @@ -1,68 +0,0 @@ -# Local Context Injection System - -Trellis context injection aims to make AI read the right files at the right time instead of relying on model memory. In a user project, injection is implemented by `.trellis/` scripts together with platform hooks, agents, and skills. - -## Injected Context Types - -| Type | Source | Purpose | -| --- | --- | --- | -| session context | `.trellis/scripts/get_context.py` | Current developer, git status, active task, active tasks, journal, packages. | -| workflow context | `.trellis/workflow.md` | Current Trellis flow and next action. | -| spec context | `.trellis/spec/` + task JSONL | Specs that must be followed during implementation/checking. | -| task context | `.trellis/tasks/<task>/prd.md`, `info.md`, `research/` | Current task requirements, design, and research. | -| platform context | Platform hooks/settings/agents | Lets different AI tools read the files above through their own mechanisms. | - -## session-start - -Platforms with session-start support inject a Trellis overview when a session starts, clears, compacts, or receives a similar event. Injected content usually includes: - -- workflow summary. -- current task status. -- active tasks. -- spec index paths. -- developer identity and git status. - -If the user feels the AI does not know the current task in a new session, first check whether the platform's session-start hook or equivalent mechanism is installed and running. - -## workflow-state - -workflow-state is a lightweight hint injected around each user turn. Based on current task status, it selects a block from `.trellis/workflow.md`, such as `no_task`, `planning`, `in_progress`, or `completed`. - -If the user wants to change "what the AI should do next in a given state," edit the corresponding state block in `.trellis/workflow.md` first. - -## sub-agent context - -Implement and check agents need task context. Trellis has two loading modes: - -1. **hook push**: a platform hook injects `prd.md` and the files referenced by `implement.jsonl` / `check.jsonl` before the agent starts. -2. **agent pull**: the agent definition instructs the agent to read the active task, PRD, and JSONL context after startup. - -In both modes, JSONL files in the task directory are the key interface. - -## JSONL Reading Rules - -`implement.jsonl` and `check.jsonl` contain one JSON object per line: - -```jsonl -{"file": ".trellis/spec/backend/index.md", "reason": "Backend rules"} -``` - -Readers should skip seed rows without a `file` field. When configuring JSONL, the AI should include only spec/research files, not pre-register code files that will be modified. - -## Active Task And Context Key - -Active task state lives in `.trellis/.runtime/sessions/` and is isolated per session. Hooks try to resolve the context key from platform events, environment variables, transcript paths, or `TRELLIS_CONTEXT_ID`. - -If shell commands cannot see the same context key, `task.py current --source` may report no active task. In that case, check whether the platform passes session identity into the shell instead of hand-writing a global current-task file. - -## Local Customization Points - -| Need | Edit location | -| --- | --- | -| Change session-start injected content | The platform's `session-start` hook or plugin file. | -| Change per-turn workflow-state rules | `[workflow-state:STATUS]` block in `.trellis/workflow.md`. The platform workflow-state hook parses these blocks verbatim and embeds no fallback text. | -| Change how sub-agents read context | Platform agent definitions, the `inject-subagent-context` hook, or agent preludes. | -| Change JSONL validation/display | `.trellis/scripts/common/task_context.py`. | -| Change active task resolution | `.trellis/scripts/common/active_task.py`. | - -When modifying context injection, verify two things: new sessions can see the correct task, and sub-agents can see the correct PRD/spec/research. diff --git a/.opencode/skills/trellis-meta/references/local-architecture/generated-files.md b/.opencode/skills/trellis-meta/references/local-architecture/generated-files.md deleted file mode 100644 index 66f832d..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/generated-files.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/local-architecture/overview.md b/.opencode/skills/trellis-meta/references/local-architecture/overview.md deleted file mode 100644 index 99c7f73..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/overview.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/local-architecture/spec-system.md b/.opencode/skills/trellis-meta/references/local-architecture/spec-system.md deleted file mode 100644 index 61281f0..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/spec-system.md +++ /dev/null @@ -1,102 +0,0 @@ -# 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/<package>/<layer>/index.md` and corresponding guideline files. | -| Change monorepo spec mapping | `packages` / `default_package` / `spec_scope` in `.trellis/config.yaml`. | -| Change which specs AI reads before implementation | The task's `implement.jsonl`. | -| Change which specs AI reads during checking | The task's `check.jsonl`. | -| Change when specs should be updated | Phase 3.3 in `.trellis/workflow.md` and the `trellis-update-spec` skill. | - -## Boundaries - -`.trellis/spec/` is the user's project specification, not a permanent copy of Trellis built-in templates. The AI should encourage the user to update it according to the actual project code instead of treating Trellis default templates as immutable documents. diff --git a/.opencode/skills/trellis-meta/references/local-architecture/task-system.md b/.opencode/skills/trellis-meta/references/local-architecture/task-system.md deleted file mode 100644 index 64ad00d..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/task-system.md +++ /dev/null @@ -1,101 +0,0 @@ -# Local Task System - -The Trellis task system is stored entirely under `.trellis/tasks/` in the user project. Each task is a directory containing requirements, context, research, state, and relationship information. - -## Task Directory Structure - -```text -.trellis/tasks/ -├── 04-28-example-task/ -│ ├── task.json -│ ├── prd.md -│ ├── info.md -│ ├── implement.jsonl -│ ├── check.jsonl -│ └── research/ -└── archive/ - └── 2026-04/ -``` - -| File | Purpose | -| --- | --- | -| `task.json` | Task metadata: status, assignee, priority, branch, parent/child tasks, and similar fields. | -| `prd.md` | Requirements document; the most important business context during implementation. | -| `info.md` | Optional technical design. | -| `implement.jsonl` | List of spec/research files the implement agent must read first. | -| `check.jsonl` | List of spec/research files the check agent must read first. | -| `research/` | Research artifacts. Complex findings should not live only in chat. | - -## `task.json` - -`task.json` records task status and metadata. Common fields: - -| Field | Meaning | -| --- | --- | -| `id` / `name` / `title` | Task identity and title. | -| `status` | Status such as `planning`, `in_progress`, `review`, or `completed`. | -| `priority` | `P0`, `P1`, `P2`, `P3`. | -| `creator` / `assignee` | Creator and assignee. | -| `package` | Target package in a monorepo; may be empty. | -| `branch` / `base_branch` | Working branch and PR target branch. | -| `children` / `parent` | Parent/child task relationships. | -| `commit` / `pr_url` | Commit and PR information after completion. | -| `meta` | Extension fields. | - -The AI should not treat phase numbers as task status. Task progress is mainly determined by `status`, `prd.md`, whether JSONL context is configured, and the phase descriptions in `workflow.md`. - -## Active Task - -The user sees a "current task," but Trellis stores active task state per session. - -```text -.trellis/.runtime/sessions/<context-key>.json -``` - -`task.py start` writes the task path into the runtime session file for the current session. `task.py current --source` shows the current task and where it came from. Different AI windows can point to different tasks without overwriting each other. - -If the platform or shell environment has no stable session identity, `task.py start` may be unable to set the active task. The AI should read the error, inspect the platform hook/session environment, and not fall back to a shared global pointer. - -## JSONL Context - -`implement.jsonl` and `check.jsonl` are context manifests for sub-agents to read first. - -Format: - -```jsonl -{"file": ".trellis/spec/cli/backend/index.md", "reason": "Backend conventions"} -{"file": ".trellis/tasks/04-28-example/research/api.md", "reason": "API research"} -``` - -Rules: - -- Include spec and research files. -- Do not include code files that are about to be modified. -- Do not treat temporary conclusions in chat as the only context. -- Seed rows have no `file` field; they only prompt the AI to fill in real entries. - -## Common Commands - -```bash -python3 ./.trellis/scripts/task.py create "<title>" --slug <slug> -python3 ./.trellis/scripts/task.py start <task> -python3 ./.trellis/scripts/task.py current --source -python3 ./.trellis/scripts/task.py add-context <task> implement <file> <reason> -python3 ./.trellis/scripts/task.py validate <task> -python3 ./.trellis/scripts/task.py finish -python3 ./.trellis/scripts/task.py archive <task> -``` - -When modifying the task system, the AI should prefer script commands to maintain structure. Edit JSON/Markdown directly only when scripts do not cover the need. - -## Local Customization Points - -| Need | Edit location | -| --- | --- | -| Change the default task template | `.trellis/scripts/common/task_store.py` and task creation instructions. | -| Change status semantics | `.trellis/workflow.md`, workflow-state hook logic, and task usage conventions. | -| Add task lifecycle actions | `hooks.after_*` in `.trellis/config.yaml`. | -| Change context rules | 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/.opencode/skills/trellis-meta/references/local-architecture/workflow.md b/.opencode/skills/trellis-meta/references/local-architecture/workflow.md deleted file mode 100644 index f0659ff..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/workflow.md +++ /dev/null @@ -1,75 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/local-architecture/workspace-memory.md b/.opencode/skills/trellis-meta/references/local-architecture/workspace-memory.md deleted file mode 100644 index c2958f2..0000000 --- a/.opencode/skills/trellis-meta/references/local-architecture/workspace-memory.md +++ /dev/null @@ -1,71 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/platform-files/agents.md b/.opencode/skills/trellis-meta/references/platform-files/agents.md deleted file mode 100644 index efbacfa..0000000 --- a/.opencode/skills/trellis-meta/references/platform-files/agents.md +++ /dev/null @@ -1,79 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/platform-files/hooks-and-settings.md b/.opencode/skills/trellis-meta/references/platform-files/hooks-and-settings.md deleted file mode 100644 index 94156a8..0000000 --- a/.opencode/skills/trellis-meta/references/platform-files/hooks-and-settings.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/platform-files/overview.md b/.opencode/skills/trellis-meta/references/platform-files/overview.md deleted file mode 100644 index 60ae1df..0000000 --- a/.opencode/skills/trellis-meta/references/platform-files/overview.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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/.opencode/skills/trellis-meta/references/platform-files/platform-map.md b/.opencode/skills/trellis-meta/references/platform-files/platform-map.md deleted file mode 100644 index b5576f4..0000000 --- a/.opencode/skills/trellis-meta/references/platform-files/platform-map.md +++ /dev/null @@ -1,74 +0,0 @@ -# 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. diff --git a/.opencode/skills/trellis-meta/references/platform-files/skills-and-commands.md b/.opencode/skills/trellis-meta/references/platform-files/skills-and-commands.md deleted file mode 100644 index 816c666..0000000 --- a/.opencode/skills/trellis-meta/references/platform-files/skills-and-commands.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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/.opencode/skills/trellis-spec-bootstarp/SKILL.md b/.opencode/skills/trellis-spec-bootstarp/SKILL.md deleted file mode 100644 index 2f7c7ad..0000000 --- a/.opencode/skills/trellis-spec-bootstarp/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -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/.opencode/skills/trellis-spec-bootstarp/references/mcp-setup.md b/.opencode/skills/trellis-spec-bootstarp/references/mcp-setup.md deleted file mode 100644 index 629fcbd..0000000 --- a/.opencode/skills/trellis-spec-bootstarp/references/mcp-setup.md +++ /dev/null @@ -1,90 +0,0 @@ -# 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/.opencode/skills/trellis-spec-bootstarp/references/repository-analysis.md b/.opencode/skills/trellis-spec-bootstarp/references/repository-analysis.md deleted file mode 100644 index 1309d29..0000000 --- a/.opencode/skills/trellis-spec-bootstarp/references/repository-analysis.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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/.opencode/skills/trellis-spec-bootstarp/references/spec-task-planning.md b/.opencode/skills/trellis-spec-bootstarp/references/spec-task-planning.md deleted file mode 100644 index dca2687..0000000 --- a/.opencode/skills/trellis-spec-bootstarp/references/spec-task-planning.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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/.opencode/skills/trellis-spec-bootstarp/references/spec-writing.md b/.opencode/skills/trellis-spec-bootstarp/references/spec-writing.md deleted file mode 100644 index 6bc7dec..0000000 --- a/.opencode/skills/trellis-spec-bootstarp/references/spec-writing.md +++ /dev/null @@ -1,70 +0,0 @@ -# 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/.opencode/skills/trellis-update-spec/SKILL.md b/.opencode/skills/trellis-update-spec/SKILL.md deleted file mode 100644 index 557bc4e..0000000 --- a/.opencode/skills/trellis-update-spec/SKILL.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -name: trellis-update-spec -description: "Captures executable contracts and coding conventions into .trellis/spec/ documents. Use when learning something valuable from debugging, implementing, or discussion that should be preserved for future sessions." ---- - -# Update Code-Spec - Capture Executable Contracts - -When you learn something valuable (from debugging, implementing, or discussion), use this to update the relevant code-spec documents. - -**Timing**: After completing a task, fixing a bug, or discovering a new pattern - ---- - -## Code-Spec First Rule (CRITICAL) - -In this project, "spec" for implementation work means **code-spec**: -- Executable contracts (not principle-only text) -- Concrete signatures, payload fields, env keys, and boundary behavior -- Testable validation/error behavior - -If the change touches infra or cross-layer contracts, code-spec depth is mandatory. - -### Mandatory Triggers - -Apply code-spec depth when the change includes any of: -- New/changed command or API signature -- Cross-layer request/response contract change -- Database schema/migration change -- Infra integration (storage, queue, cache, secrets, env wiring) - -### Mandatory Output (7 Sections) - -For triggered tasks, include all sections below: -1. Scope / Trigger -2. Signatures (command/API/DB) -3. Contracts (request/response/env) -4. Validation & Error Matrix -5. Good/Base/Bad Cases -6. Tests Required (with assertion points) -7. Wrong vs Correct (at least one pair) - ---- - -## When to Update Code-Specs - -| Trigger | Example | Target Spec | -|---------|---------|-------------| -| **Implemented a feature** | Added a new integration or module | Relevant spec file | -| **Made a design decision** | Chose extensibility pattern over simplicity | Relevant spec + "Design Decisions" section | -| **Fixed a bug** | Found a subtle issue with error handling | Relevant spec (e.g., error-handling docs) | -| **Discovered a pattern** | Found a better way to structure code | Relevant spec file | -| **Hit a gotcha** | Learned that X must be done before Y | Relevant spec + "Common Mistakes" section | -| **Established a convention** | Team agreed on naming pattern | Quality guidelines | -| **New thinking trigger** | "Don't forget to check X before doing Y" | `guides/*.md` (as a checklist item) | - -**Key Insight**: Code-spec updates are NOT just for problems. Every feature implementation contains design decisions and contracts that future AI/developers need to execute safely. - ---- - -## Spec Structure Overview - -``` -.trellis/spec/ -├── <layer>/ # Per-layer coding standards (e.g., backend/, frontend/, api/) -│ ├── index.md # Overview and links -│ └── *.md # Topic-specific guidelines -└── guides/ # Thinking checklists (NOT coding specs!) - ├── index.md # Guide index - └── *.md # Topic-specific guides -``` - -### CRITICAL: Code-Spec vs Guide - Know the Difference - -| Type | Location | Purpose | Content Style | -|------|----------|---------|---------------| -| **Code-Spec** | `<layer>/*.md` | Tell AI "how to implement safely" | Signatures, contracts, matrices, cases, test points | -| **Guide** | `guides/*.md` | Help AI "what to think about" | Checklists, questions, pointers to specs | - -**Decision Rule**: Ask yourself: - -- "This is **how to write** the code" → Put in a spec layer directory -- "This is **what to consider** before writing" → Put in `guides/` - -**Example**: - -| Learning | Wrong Location | Correct Location | -|----------|----------------|------------------| -| "Use API X not API Y for this task" | ❌ `guides/` (too specific for a thinking guide) | ✅ Relevant spec file (concrete convention) | -| "Remember to check X when doing Y" | ❌ Spec file (too abstract for a spec) | ✅ `guides/` (thinking checklist) | - -**Guides should be short checklists that point to specs**, not duplicate the detailed rules. - ---- - -## Update Process - -### Step 1: Identify What You Learned - -Answer these questions: - -1. **What did you learn?** (Be specific) -2. **Why is it important?** (What problem does it prevent?) -3. **Where does it belong?** (Which spec file?) - -### Step 2: Classify the Update Type - -| Type | Description | Action | -|------|-------------|--------| -| **Design Decision** | Why we chose approach X over Y | Add to "Design Decisions" section | -| **Project Convention** | How we do X in this project | Add to relevant section with examples | -| **New Pattern** | A reusable approach discovered | Add to "Patterns" section | -| **Forbidden Pattern** | Something that causes problems | Add to "Anti-patterns" or "Don't" section | -| **Common Mistake** | Easy-to-make error | Add to "Common Mistakes" section | -| **Convention** | Agreed-upon standard | Add to relevant section | -| **Gotcha** | Non-obvious behavior | Add warning callout | - -### Step 3: Read the Target Code-Spec - -Before editing, read the current code-spec to: -- Understand existing structure -- Avoid duplicating content -- Find the right section for your update - -```bash -cat .trellis/spec/<category>/<file>.md -``` - -### Step 4: Make the Update - -Follow these principles: - -1. **Be Specific**: Include concrete examples, not just abstract rules -2. **Explain Why**: State the problem this prevents -3. **Show Contracts**: Add signatures, payload fields, and error behavior -4. **Show Code**: Add code snippets for key patterns -5. **Keep it Short**: One concept per section - -### Step 5: Update the Index (if needed) - -If you added a new section or the code-spec status changed, update the category's `index.md`. - ---- - -## Update Templates - -### Mandatory Template for Infra/Cross-Layer Work - -```markdown -## Scenario: <name> - -### 1. Scope / Trigger -- Trigger: <why this requires code-spec depth> - -### 2. Signatures -- Backend command/API/DB signature(s) - -### 3. Contracts -- Request fields (name, type, constraints) -- Response fields (name, type, constraints) -- Environment keys (required/optional) - -### 4. Validation & Error Matrix -- <condition> -> <error> - -### 5. Good/Base/Bad Cases -- Good: ... -- Base: ... -- Bad: ... - -### 6. Tests Required -- Unit/Integration/E2E with assertion points - -### 7. Wrong vs Correct -#### Wrong -... -#### Correct -... -``` - -### Adding a Design Decision - -```markdown -### Design Decision: [Decision Name] - -**Context**: What problem were we solving? - -**Options Considered**: -1. Option A - brief description -2. Option B - brief description - -**Decision**: We chose Option X because... - -**Example**: -\`\`\`typescript -// How it's implemented -code example -\`\`\` - -**Extensibility**: How to extend this in the future... -``` - -### Adding a Project Convention - -```markdown -### Convention: [Convention Name] - -**What**: Brief description of the convention. - -**Why**: Why we do it this way in this project. - -**Example**: -\`\`\`typescript -// How to follow this convention -code example -\`\`\` - -**Related**: Links to related conventions or specs. -``` - -### Adding a New Pattern - -```markdown -### Pattern Name - -**Problem**: What problem does this solve? - -**Solution**: Brief description of the approach. - -**Example**: -\`\`\` -// Good -code example - -// Bad -code example -\`\`\` - -**Why**: Explanation of why this works better. -``` - -### Adding a Forbidden Pattern - -```markdown -### Don't: Pattern Name - -**Problem**: -\`\`\` -// Don't do this -bad code example -\`\`\` - -**Why it's bad**: Explanation of the issue. - -**Instead**: -\`\`\` -// Do this instead -good code example -\`\`\` -``` - -### Adding a Common Mistake - -```markdown -### Common Mistake: Description - -**Symptom**: What goes wrong - -**Cause**: Why this happens - -**Fix**: How to correct it - -**Prevention**: How to avoid it in the future -``` - -### Adding a Gotcha - -```markdown -> **Warning**: Brief description of the non-obvious behavior. -> -> Details about when this happens and how to handle it. -``` - ---- - -## Interactive Mode - -If you're unsure what to update, answer these prompts: - -1. **What did you just finish?** - - [ ] Fixed a bug - - [ ] Implemented a feature - - [ ] Refactored code - - [ ] Had a discussion about approach - -2. **What did you learn or decide?** - - Design decision (why X over Y) - - Project convention (how we do X) - - Non-obvious behavior (gotcha) - - Better approach (pattern) - -3. **Would future AI/developers need to know this?** - - To understand how the code works → Yes, update spec - - To maintain or extend the feature → Yes, update spec - - To avoid repeating mistakes → Yes, update spec - - Purely one-off implementation detail → Maybe skip - -4. **Which area does it relate to?** - - [ ] Backend code - - [ ] Frontend code - - [ ] Cross-layer data flow - - [ ] Code organization/reuse - - [ ] Quality/testing - ---- - -## Quality Checklist - -Before finishing your code-spec update: - -- [ ] Is the content specific and actionable? -- [ ] Did you include a code example? -- [ ] Did you explain WHY, not just WHAT? -- [ ] Did you include executable signatures/contracts? -- [ ] Did you include validation and error matrix? -- [ ] Did you include Good/Base/Bad cases? -- [ ] Did you include required tests with assertion points? -- [ ] Is it in the right code-spec file? -- [ ] Does it duplicate existing content? -- [ ] Would a new team member understand it? - ---- - -## Relationship to Other Commands - -``` -Development Flow: - Learn something → /trellis:update-spec → Knowledge captured - ↑ ↓ - /trellis:break-loop ←──────────────────── Future sessions benefit - (deep bug analysis) -``` - -- `/trellis:break-loop` - Analyzes bugs deeply, often reveals spec updates needed -- `/trellis:update-spec` - Actually makes the updates -- `/trellis:finish-work` - Reminds you to check if specs need updates - ---- - -## Core Philosophy - -> **Code-specs are living documents. Every debugging session, every "aha moment" is an opportunity to make the implementation contract clearer.** - -The goal is **institutional memory**: -- What one person learns, everyone benefits from -- What AI learns in one session, persists to future sessions -- Mistakes become documented guardrails diff --git a/.trellis/.gitignore b/.trellis/.gitignore deleted file mode 100644 index 5a991ea..0000000 --- a/.trellis/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Developer identity (local only) -.developer - -# Current task pointer (each dev works on different task) -.current-task - -# Session/window scoped runtime state -.runtime/ - -# Ralph Loop state file -.ralph-state.json - -# Agent runtime files -.agents/ -.agent-log -.session-id - -# Task directory runtime files -.plan-log - -# Atomic update temp files -*.tmp - -# Update backup directories -.backup-* - -# Conflict resolution temp files -*.new - -# Python cache -**/__pycache__/ -**/*.pyc diff --git a/.trellis/.template-hashes.json b/.trellis/.template-hashes.json deleted file mode 100644 index c86cd21..0000000 --- a/.trellis/.template-hashes.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "__version": 2, - "hashes": { - ".claude/agents/trellis-check.md": "d1359521f7f3e9bbbf10e856a3e0912c423581a88ac188b1f0523d6357962909", - ".claude/agents/trellis-implement.md": "61155f06ccdd26e5aeb8171face2a029a8fb77a3d1a2b277442ded186853446c", - ".claude/agents/trellis-research.md": "f82244b2a88a1f77b09813f58a7fdf506f8e9603a5c34b3abe6e170f73ab68a8", - ".claude/settings.json": "d13cd05659281a287d7f50c7e25eb6a89c2a6597773511bd6885538acced2855", - ".claude/hooks/inject-subagent-context.py": "3f2bafe1af36803aba1ad50947104aed817d77918540a4025db73aa0b249e3a2", - ".claude/hooks/inject-workflow-state.py": "fe4cca4db7ca8c252f614efca16fa588009a4238542a2c9aa85167409f6f9a4c", - ".claude/hooks/session-start.py": "b905a58b2232284883623bf38567c8170eec2ab60e3ddfb89040078eafdd3317", - ".claude/commands/trellis/continue.md": "0683093a150d189af02c5de33afe01ebb740e940f9325f7ba7390906895661a1", - ".claude/commands/trellis/finish-work.md": "d6aa570ab684f57e4845de2d84a1ff6d9f0908e04c5a56e14fd70ae739c369fc", - ".claude/skills/trellis-before-dev/SKILL.md": "208ad3fd5131fa0da603d4bc354a29826967397f5aeef483fa0564113df13e9e", - ".claude/skills/trellis-brainstorm/SKILL.md": "ca6c0586f5150e22be0bf4c0341b49dd2c6da363e62839aaa33996c79bad2b7d", - ".claude/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f", - ".claude/skills/trellis-check/SKILL.md": "a3f17aef687aa3b475d12ee64c3293e5491bb7474336be2c0f9ec22042f13b6e", - ".claude/skills/trellis-update-spec/SKILL.md": "d975db7af166578488958751ae2c56edb827a68bddb569aa27acc3453f64e610", - ".claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", - ".claude/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c", - ".claude/skills/trellis-meta/references/customize-local/change-context-loading.md": "c7be53e038eac99ff4d0abb3fafc67cfdfd90352744ce09ad11e7ef5085cf933", - ".claude/skills/trellis-meta/references/customize-local/change-hooks.md": "91892f2cff53ae003736007e95172945acabf48b2fc889bd627cd2406ce449c4", - ".claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea", - ".claude/skills/trellis-meta/references/customize-local/change-spec-structure.md": "a3fc9da294448b4a3525ae1ab7c8c9b895ef8f3b53bcf37a13ce6afbdc040fe6", - ".claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", - ".claude/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb", - ".claude/skills/trellis-meta/references/customize-local/overview.md": "1a406c24b4c5737cf517ead5ecb0846c20b0648a117008d3f5a47614fc1793ca", - ".claude/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375", - ".claude/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f", - ".claude/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f", - ".claude/skills/trellis-meta/references/local-architecture/spec-system.md": "8996504201e2a5460516948fe29c4ce7ed6b960f15da8c514855d2d467046985", - ".claude/skills/trellis-meta/references/local-architecture/task-system.md": "a6f905e53fe5a7fa5be8a4bbe1d102bd90b69c6e63a1a3be51ac3f7f164f1d21", - ".claude/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", - ".claude/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", - ".claude/skills/trellis-meta/references/platform-files/agents.md": "ffee78fc3c29114c7409ca4805ebd51d44d8a6acb630e9f73d47013e10db5ac5", - ".claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462", - ".claude/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d", - ".claude/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212", - ".claude/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f", - ".claude/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34", - ".claude/skills/trellis-spec-bootstarp/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", - ".claude/skills/trellis-spec-bootstarp/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", - ".claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", - ".claude/skills/trellis-spec-bootstarp/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", - ".claude/skills/trellis-spec-bootstarp/SKILL.md": "81f400092b21392161e7d9dfa9111c9be36c81bc8641d252ed28e08373449ac0", - ".opencode/agents/trellis-check.md": "4b31ab1330403495f7a72efa9f5fe63d03d94d27b0be4a1274cd0ab38268a303", - ".opencode/agents/trellis-implement.md": "f5b0712186e4bf765a4a32acd46ae31699ebf8742e2a0afb733402994214f485", - ".opencode/agents/trellis-research.md": "2c5135aefe280fd4508554e58c64bd13f5f9fe58b8bb25393e68496b29bfae4e", - ".opencode/lib/session-utils.js": "2d9ba3440bc1e000ab14e0657cbfc9063902bfa2798499cbff124691f7cdb407", - ".opencode/lib/trellis-context.js": "778ffc725b04ba50e950c379ed7a72a3796acbf946addc21837e7f30b60205f1", - ".opencode/package.json": "4b155e844fde1467e331e898b378e66820c323110ef1ecae6fce3844358535ea", - ".opencode/plugins/inject-subagent-context.js": "5a35856f746ffe2bc97f67daa61c68eb8025149bbb28cc8f6d6093adccea293c", - ".opencode/plugins/inject-workflow-state.js": "94e1cc76a4b55bf97c7cfb8ec10b5f5c848850fffdddf7060ed26285ab3be6da", - ".opencode/plugins/session-start.js": "798b22cbe6c7f3e1a532e322891daed4c00de08951dee06858773ca122c254f1", - ".opencode/commands/trellis/continue.md": "37856640afa49207cf5b09f8a1cdf51d8d7c7de0b7d22ef88aaf65c51f9577be", - ".opencode/commands/trellis/finish-work.md": "d6aa570ab684f57e4845de2d84a1ff6d9f0908e04c5a56e14fd70ae739c369fc", - ".opencode/skills/trellis-before-dev/SKILL.md": "208ad3fd5131fa0da603d4bc354a29826967397f5aeef483fa0564113df13e9e", - ".opencode/skills/trellis-brainstorm/SKILL.md": "ca6c0586f5150e22be0bf4c0341b49dd2c6da363e62839aaa33996c79bad2b7d", - ".opencode/skills/trellis-break-loop/SKILL.md": "35afb53fef42cd494e566f1ef170dbf442ec2be7e19931f28a14079b4dda753f", - ".opencode/skills/trellis-check/SKILL.md": "a3f17aef687aa3b475d12ee64c3293e5491bb7474336be2c0f9ec22042f13b6e", - ".opencode/skills/trellis-update-spec/SKILL.md": "d975db7af166578488958751ae2c56edb827a68bddb569aa27acc3453f64e610", - ".opencode/skills/trellis-meta/references/customize-local/add-project-local-conventions.md": "86009ccb5d0373f399582da0bc570c4e5c6053c3c764857424ff93384f0e04e5", - ".opencode/skills/trellis-meta/references/customize-local/change-agents.md": "7f2982162463f107f8b1a4fa1a41fee2bc7dbd0cc8e90c48559aba30c3ea403c", - ".opencode/skills/trellis-meta/references/customize-local/change-context-loading.md": "c7be53e038eac99ff4d0abb3fafc67cfdfd90352744ce09ad11e7ef5085cf933", - ".opencode/skills/trellis-meta/references/customize-local/change-hooks.md": "91892f2cff53ae003736007e95172945acabf48b2fc889bd627cd2406ce449c4", - ".opencode/skills/trellis-meta/references/customize-local/change-skills-or-commands.md": "b3009ef20a4f24e5d8b196109dc9bab6bd30fc030dbc4fb796afdd2ca912e1ea", - ".opencode/skills/trellis-meta/references/customize-local/change-spec-structure.md": "a3fc9da294448b4a3525ae1ab7c8c9b895ef8f3b53bcf37a13ce6afbdc040fe6", - ".opencode/skills/trellis-meta/references/customize-local/change-task-lifecycle.md": "60ff9efb93604b87a461a4af30322d76750402a51e40f31531a7ff88d309996d", - ".opencode/skills/trellis-meta/references/customize-local/change-workflow.md": "6f1707a2cc032c50e41e5624cef46071dd53dc9810bc6b3cae66d86508dea1cb", - ".opencode/skills/trellis-meta/references/customize-local/overview.md": "1a406c24b4c5737cf517ead5ecb0846c20b0648a117008d3f5a47614fc1793ca", - ".opencode/skills/trellis-meta/references/local-architecture/context-injection.md": "31286b9c05e600db7d179100eca533f9b8a4aab3a9c255cb69e8dccacb4e8375", - ".opencode/skills/trellis-meta/references/local-architecture/generated-files.md": "4356517517cef0ba7f3ba01965a4ba8953505702e4085f0797d3e36817c9669f", - ".opencode/skills/trellis-meta/references/local-architecture/overview.md": "45ffd4ee95020f58201adc885f3dfc89b26483c2b350d96ca7f2f57f94d5ff5f", - ".opencode/skills/trellis-meta/references/local-architecture/spec-system.md": "8996504201e2a5460516948fe29c4ce7ed6b960f15da8c514855d2d467046985", - ".opencode/skills/trellis-meta/references/local-architecture/task-system.md": "a6f905e53fe5a7fa5be8a4bbe1d102bd90b69c6e63a1a3be51ac3f7f164f1d21", - ".opencode/skills/trellis-meta/references/local-architecture/workflow.md": "cfcdc6e4468a5d9c816e929fcca01640cd41cfdaaa4824118b40a8e460c927b6", - ".opencode/skills/trellis-meta/references/local-architecture/workspace-memory.md": "e6427b46aba744563c2444b30df4043cd856561b7709ec2dece26095416421fd", - ".opencode/skills/trellis-meta/references/platform-files/agents.md": "ffee78fc3c29114c7409ca4805ebd51d44d8a6acb630e9f73d47013e10db5ac5", - ".opencode/skills/trellis-meta/references/platform-files/hooks-and-settings.md": "6e2d6d88719c2779fe34004f63d36cff203d8f64e7fb620f7cb1cde15c37c462", - ".opencode/skills/trellis-meta/references/platform-files/overview.md": "6479cd2393166b4b369b511c44b78cbc64975c8b1df96ee1d4d1bd06b75cd48d", - ".opencode/skills/trellis-meta/references/platform-files/platform-map.md": "ded6751c06f31d0a701d33c9dd69c482a583539ad3ed464aaad9e705f793b212", - ".opencode/skills/trellis-meta/references/platform-files/skills-and-commands.md": "85435eb8bb6921283575bca51268fc534c22fd3ca33782e841ee5c76140ae48f", - ".opencode/skills/trellis-meta/SKILL.md": "942e898a6fd769a93a3ca6f43f9fe0412d0adae011654fd384e9cacbd2af4f34", - ".opencode/skills/trellis-spec-bootstarp/references/mcp-setup.md": "df542fc8f279edd38046d26a7c8151804b708f57b24d4aa2733cea587a88c65e", - ".opencode/skills/trellis-spec-bootstarp/references/repository-analysis.md": "0dae98d774f6e34559b9f3442888ac43e3a8af110c37cbefc49ce256986858b6", - ".opencode/skills/trellis-spec-bootstarp/references/spec-task-planning.md": "ef493d028c3b0807a8a534bb71fb92a68129f273db763ad27ceb464a522e799d", - ".opencode/skills/trellis-spec-bootstarp/references/spec-writing.md": "e9800fe9ed4a4cd87062ea1829cf2caa8d170ec15e141678a6a30e74c497f47d", - ".opencode/skills/trellis-spec-bootstarp/SKILL.md": "81f400092b21392161e7d9dfa9111c9be36c81bc8641d252ed28e08373449ac0", - "AGENTS.md": "6cacfe99748b435d0660c2463c697bc323d53798aecf3492283ca8eac1b29682", - ".trellis/config.yaml": "5c9207418cecc390e9d86d589b4183b831f76697d92fb42fefd5221cd8772e51", - ".trellis/scripts/__init__.py": "1242be5b972094c2e141aecbe81a4efd478f6534e3d5e28306374e6a18fcf46c", - ".trellis/scripts/add_session.py": "6e406a0a9f32d4a50b1b5ca8115cbd06c359011f0e166c41dc5fab34698a4006", - ".trellis/scripts/common/__init__.py": "3d5e9347141f0296319a5beb29d69ae714c5a474b9078caeb3edd7c5f6562e22", - ".trellis/scripts/common/active_task.py": "6c88ed40ef7289bca0f6d2ecba0f8b8aef46cd58788080fbeeea88de138a431f", - ".trellis/scripts/common/cli_adapter.py": "cd844d1e84b1a09b373b3a7609e4d5606ee9d4825154c002cc9bb3f54c8e2fb9", - ".trellis/scripts/common/config.py": "25c5a53ad20d6909be5209222e4208a84528805316a4d78350529459a364edb1", - ".trellis/scripts/common/developer.py": "f5f833123abe68890171b4da825a324216d24913f6b5ad9245afc556424ffd7b", - ".trellis/scripts/common/git.py": "e14817be7de122d3a106f509c2825aeb9669d962ba73ba241642d2931cfdf1d6", - ".trellis/scripts/common/git_context.py": "fa30ced454f1a91ffc9f8b2abeb32225e3447cbdc90bad783797374eba07265d", - ".trellis/scripts/common/io.py": "6480b181f2bc505323b28ed7a66963d7b7edc96251e83b4c8e7a45907cc721c8", - ".trellis/scripts/common/log.py": "471df6895cfac80f995edebbf9974f6b7440634b7a688f28b8331c868bc0f3cf", - ".trellis/scripts/common/packages_context.py": "efe158d7c99c2268851d0216fbb08de22836e418a8dbeb73575b8cc249eed7b7", - ".trellis/scripts/common/paths.py": "05898ef136cc7c4d861b05fbf2b16d53ddd3e6f311a231d4fcfcb81bde7c45ee", - ".trellis/scripts/common/safe_commit.py": "8789bff4b30a9065469210f2efab3f59f03dddd77bef4e4b6a5bb641f93539f4", - ".trellis/scripts/common/session_context.py": "c91cf213025e5e4ad2f56bb6f570ef43d35430fc8a863be311d3e5f8f32ef652", - ".trellis/scripts/common/task_context.py": "1c16a7fa82d363010d0d0ebdc038296ae1552bf6e90214787d707f49567bc159", - ".trellis/scripts/common/task_queue.py": "0be61f713462b1fe4574927c82fc4704e678afe72dcb9813543aedf2f9e9e0c5", - ".trellis/scripts/common/task_store.py": "cca4851c948df4a7d8dac38c465dce0d6b15a171001adab8dfef011ad66e02e5", - ".trellis/scripts/common/task_utils.py": "f5ef4af87ba3e11d8b19630c0c96d009de1811fc9be56c2027a9c96e21ed103e", - ".trellis/scripts/common/tasks.py": "4436a8b0b53c270a35989e26d9dbd92669408c6562d88c02083a404562da85fe", - ".trellis/scripts/common/trellis_config.py": "0839dcf90ebbd77712c276930a89335b3313927051650c91d220fb51ca2a6a3c", - ".trellis/scripts/common/types.py": "9962081cc2608fb9d1deb32c6880e336f62cdca6b338e7ae813304701e155ee9", - ".trellis/scripts/common/workflow_phase.py": "2b260f4a7770e9c3223129836716bd8e2c0f0568acd682224a57415bc1dc726b", - ".trellis/scripts/get_context.py": "ca5bf9e90bdb1d75d3de182b95f820f9d108ab28793d29097b24fd71315adcf5", - ".trellis/scripts/get_developer.py": "84c27076323c3e0f2c9c8ed16e8aa865e225d902a187c37e20ee1a46e7142d8f", - ".trellis/scripts/hooks/linear_sync.py": "e09cc4ce4699aada908808718698f33f705a3edf55c4dcf8f777ad892f80ca79", - ".trellis/scripts/init_developer.py": "f9e6c0d882406e81c8cd6b1c5abb204b0befc0069ff89cf650cd536a80f8c60e", - ".trellis/scripts/task.py": "40abdd46f5c2b6837610429a38eef50f1fc783fb1852dc4f52a891205e42ab04", - ".trellis/workflow.md": "f4b8a6f89017f62071986d6d36cc32c4c7f01fe6f023f0fd9311eddc57d8c94f", - ".trellis/.runtime/sessions/opencode_ses_0fa9bd669ffeYrt7zm7OK1GYM8.json": "0057637c43c0458e064360a5cdfb047da4a4107cf3e91bdcf9dab39f909af4b0", - ".trellis/.runtime/update-check-claude_2ab4f024-6de1-48a9-a1e6-ae4f26b174a7.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-claude_4ff99829-c447-424f-a6b9-a34607b049fa.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-claude_d9118ffd-8f45-42ea-9801-d82379b3c23a.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_0fa9ba12affeWK76gGIDEf0tDZ.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_0fa9baae2ffeqp0asVrd8rcWFl.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_0fa9bd669ffeYrt7zm7OK1GYM8.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_0fff4a7a5ffeSofnz1j0YF4O56.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_1005858f0ffeXfy2McZGWqgoG2.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_1019cfb14ffewFpp4foONH6Wu6.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/.runtime/update-check-opencode_ses_1019d29b8ffepGPHFwLKNJ0HlH.marker": "77c2ca150b61c7330da139378ffd3940d093f1bd74a1294689345d27e15b5124", - ".trellis/scripts/common/__pycache__/__init__.cpython-313.pyc": "c507bcf29f62120973aca3d193c223167ec29838a39b00c56748dfc3f431c6c7", - ".trellis/scripts/common/__pycache__/active_task.cpython-313.pyc": "3c357180210e0e2f2041ca45b56614e161959ab4809d72ebee33f43b9ac2aad8", - ".trellis/scripts/common/__pycache__/config.cpython-313.pyc": "12eaa5b7207125527c503acbd5620afede4a80e1ac1a7eee2091a928db8270b3", - ".trellis/scripts/common/__pycache__/developer.cpython-313.pyc": "0ef11d0d4912e31bf444d43584186ae22e5c94a915b6bb73bb1c5a6565c2f906", - ".trellis/scripts/common/__pycache__/git.cpython-313.pyc": "df26e82d048e295eee4c1cbfa4bb2e7f109bd44bb5aba37ef8a2d40174609bd8", - ".trellis/scripts/common/__pycache__/git_context.cpython-313.pyc": "ef792d63110597d6c1f0eabce437a4c5eecaf86d7582255c95f5d7c83a9a89a4", - ".trellis/scripts/common/__pycache__/io.cpython-313.pyc": "8d20ac0339b6b0e6a4457502844d13d8b050250ff7021e55fcd8bff61d987434", - ".trellis/scripts/common/__pycache__/log.cpython-313.pyc": "67224fab1a76d37d942f019b3d2a875c6497bb1d582f9dcc478e49c0c69d216a", - ".trellis/scripts/common/__pycache__/packages_context.cpython-313.pyc": "4ee8a87d8112d233d8310b1364223ec13835e378d2aa62fe84139d089e28b5f6", - ".trellis/scripts/common/__pycache__/paths.cpython-313.pyc": "a01b7eaf9ee4a61e42a4157da475e28655dec54ac2804895e052308dc91e8bca", - ".trellis/scripts/common/__pycache__/safe_commit.cpython-313.pyc": "b42206d0d64a1ea72548e88d9730f186334566af1e1bff58d095845db37db407", - ".trellis/scripts/common/__pycache__/session_context.cpython-313.pyc": "66dc9d914a744bb697f41f9187df38e8a00d39f93b2f73fc20cb3d3b112c2275", - ".trellis/scripts/common/__pycache__/task_context.cpython-313.pyc": "7de2ac31464c7502db0a147e9d5758906a9ba7e4ae54e5e9e289e95730956d33", - ".trellis/scripts/common/__pycache__/task_store.cpython-313.pyc": "29ae8e0e8b13f55b84a6271df2752b439bf4df7e6edba1f4ab77f69df04caa4c", - ".trellis/scripts/common/__pycache__/task_utils.cpython-313.pyc": "122c0d3bb365524ed6523f5e5ca5168b0e119a28000a9bf16a7a2d4ad3d5b7c3", - ".trellis/scripts/common/__pycache__/tasks.cpython-313.pyc": "b9db435c1953f006a6065881b8c1e1c350e010253930de0df4eea4f3062e51ed", - ".trellis/scripts/common/__pycache__/trellis_config.cpython-313.pyc": "fede1256e0abfe954c965e605b6bd6eb6cf9f062c47e9215e82879a5c0fa6f3c", - ".trellis/scripts/common/__pycache__/types.cpython-313.pyc": "9c3ac38bf04b034a8a6fb5c3dbd37918bac6bce28123c760d06c80a9b8502b75", - ".trellis/scripts/common/__pycache__/workflow_phase.cpython-313.pyc": "eed71dc42941cbccbf2d4c38673c9ec465286a03163bcebc39e1c4ca0bbb3dab" - } -} \ No newline at end of file diff --git a/.trellis/.version b/.trellis/.version deleted file mode 100644 index aa49d36..0000000 --- a/.trellis/.version +++ /dev/null @@ -1 +0,0 @@ -0.5.19 \ No newline at end of file diff --git a/.trellis/config.yaml b/.trellis/config.yaml deleted file mode 100644 index f1e99eb..0000000 --- a/.trellis/config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Trellis Configuration -# Project-level settings for the Trellis workflow system -# -# All values have sensible defaults. Only override what you need. - -#------------------------------------------------------------------------------- -# Session Recording -#------------------------------------------------------------------------------- - -# Commit message used when auto-committing journal/index changes -# after running add_session.py -session_commit_message: "chore: record journal" - -# Maximum lines per journal file before rotating to a new one -max_journal_lines: 2000 - -#------------------------------------------------------------------------------- -# Session Auto-Commit -#------------------------------------------------------------------------------- - -# Auto-commit behavior for session journal + task archive operations. -# - true (default): scripts auto-stage and auto-commit journal / task changes -# after add_session.py / task.py archive runs. -# - false: scripts do not touch git. Files (journal-*.md, task archive moves) -# are still written to disk; you decide whether to git add / commit. -# -# Use `false` if your project's .gitignore intentionally excludes `.trellis/` -# and you want session data kept local-only, or if you prefer to review -# staged changes manually before each commit. -# -# Accepts: true / false / yes / no / 1 / 0 / on / off (case-insensitive). -# -# session_auto_commit: true - -#------------------------------------------------------------------------------- -# Task Lifecycle Hooks -#------------------------------------------------------------------------------- - -# Shell commands to run after task lifecycle events. -# Each hook receives TASK_JSON_PATH environment variable pointing to task.json. -# Hook failures print a warning but do not block the main operation. -# -# hooks: -# after_create: -# - "echo 'Task created'" -# after_start: -# - "echo 'Task started'" -# after_finish: -# - "echo 'Task finished'" -# after_archive: -# - "echo 'Task archived'" - -#------------------------------------------------------------------------------- -# Monorepo / Packages -#------------------------------------------------------------------------------- - -# Declare packages for monorepo projects. -# Trellis auto-detects workspaces during `trellis init`, but you can also -# configure them manually here. -# -# packages: -# frontend: -# path: packages/frontend -# backend: -# path: packages/backend -# docs: -# path: docs-site -# type: submodule -# # For polyrepo / meta-repo layouts (independent .git in each subdir), -# # mark the package with `git: true`. The runtime treats it as an -# # independent repository for things like git-context display. -# webapp: -# path: ./webapp -# git: true - -# Default package used when --package is not specified. -# default_package: frontend - -#------------------------------------------------------------------------------- -# Codex (dispatch behavior) -#------------------------------------------------------------------------------- -# Codex-only knob; other platforms ignore it. Default ("inline") makes the -# main Codex agent edit code directly because Codex sub-agents run with -# `fork_turns="none"` isolation and can't inherit the parent session's -# task context. Set to "sub-agent" to opt into the legacy dispatch model -# (main agent spawns trellis-implement / trellis-check / trellis-research -# sub-agents). -# -# codex: -# dispatch_mode: inline # or "sub-agent" to dispatch trellis-* sub-agents diff --git a/.trellis/scripts/__init__.py b/.trellis/scripts/__init__.py deleted file mode 100755 index 815a137..0000000 --- a/.trellis/scripts/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Trellis Python Scripts - -This module provides Python implementations of Trellis workflow scripts. -""" diff --git a/.trellis/scripts/add_session.py b/.trellis/scripts/add_session.py deleted file mode 100755 index 60c653e..0000000 --- a/.trellis/scripts/add_session.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Add a new session to journal file and update index.md. - -Usage: - python3 add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli] - python3 add_session.py --title "Title" --branch "feat/my-branch" - - # Pipe detailed content via stdin (use --stdin to opt in): - cat << 'EOF' | python3 add_session.py --stdin --title "Title" --summary "Summary" - <session content here> - EOF - -Branch resolution order: - 1. --branch CLI arg (explicit) - 2. task.json branch field (from active task) - 3. git branch --show-current (auto-detect) - 4. None (omitted gracefully) -""" - -from __future__ import annotations - -import argparse -import re -import sys -from datetime import datetime -from pathlib import Path - -from common.paths import ( - FILE_JOURNAL_PREFIX, - get_repo_root, - get_current_task, - get_developer, - get_workspace_dir, -) -from common.developer import ensure_developer -from common.git import run_git -from common.safe_commit import ( - print_gitignore_warning, - safe_git_add, - safe_trellis_paths_to_add, -) -from common.tasks import load_task -from common.config import ( - get_packages, - get_session_auto_commit, - get_session_commit_message, - get_max_journal_lines, - is_monorepo, - resolve_package, - validate_package, -) - - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]: - """Get latest journal file info. - - Returns: - Tuple of (file_path, file_number, line_count). - """ - latest_file: Path | None = None - latest_num = -1 - - for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"): - if not f.is_file(): - continue - - match = re.search(r"(\d+)$", f.stem) - if match: - num = int(match.group(1)) - if num > latest_num: - latest_num = num - latest_file = f - - if latest_file: - lines = len(latest_file.read_text(encoding="utf-8").splitlines()) - return latest_file, latest_num, lines - - return None, 0, 0 - - -def get_current_session(index_file: Path) -> int: - """Get current session number from index.md.""" - if not index_file.is_file(): - return 0 - - content = index_file.read_text(encoding="utf-8") - for line in content.splitlines(): - if "Total Sessions" in line: - match = re.search(r":\s*(\d+)", line) - if match: - return int(match.group(1)) - return 0 - - -def _extract_journal_num(filename: str) -> int: - """Extract journal number from filename for sorting.""" - match = re.search(r"(\d+)", filename) - return int(match.group(1)) if match else 0 - - -def count_journal_files(dev_dir: Path, active_num: int) -> str: - """Count journal files and return table rows.""" - active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md" - result_lines = [] - - files = sorted( - [f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()], - key=lambda f: _extract_journal_num(f.stem), - reverse=True - ) - - for f in files: - filename = f.name - lines = len(f.read_text(encoding="utf-8").splitlines()) - status = "Active" if filename == active_file else "Archived" - result_lines.append(f"| `{filename}` | ~{lines} | {status} |") - - return "\n".join(result_lines) - - -def create_new_journal_file( - dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000, -) -> Path: - """Create a new journal file.""" - prev_num = num - 1 - new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md" - - content = f"""# Journal - {developer} (Part {num}) - -> Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines) -> Started: {today} - ---- - -""" - new_file.write_text(content, encoding="utf-8") - return new_file - - -def generate_session_content( - session_num: int, - title: str, - commit: str, - summary: str, - extra_content: str, - today: str, - package: str | None = None, - branch: str | None = None, -) -> str: - """Generate session content.""" - if commit and commit != "-": - commit_table = """| Hash | Message | -|------|---------|""" - for c in commit.split(","): - c = c.strip() - commit_table += f"\n| `{c}` | (see git log) |" - else: - commit_table = "(No commits - planning session)" - - package_line = f"\n**Package**: {package}" if package else "" - branch_line = f"\n**Branch**: `{branch}`" if branch else "" - - return f""" - -## Session {session_num}: {title} - -**Date**: {today} -**Task**: {title}{package_line}{branch_line} - -### Summary - -{summary} - -### Main Changes - -{extra_content} - -### Git Commits - -{commit_table} - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete -""" - - -def update_index( - index_file: Path, - dev_dir: Path, - title: str, - commit: str, - new_session: int, - active_file: str, - today: str, - branch: str | None = None, -) -> bool: - """Update index.md with new session info.""" - # Format commit for display - commit_display = "-" - if commit and commit != "-": - commit_display = re.sub(r"([a-f0-9]{7,})", r"`\1`", commit.replace(",", ", ")) - - # Get file number from active_file name - match = re.search(r"(\d+)", active_file) - active_num = int(match.group(1)) if match else 0 - files_table = count_journal_files(dev_dir, active_num) - - print(f"Updating index.md for session {new_session}...") - print(f" Title: {title}") - print(f" Commit: {commit_display}") - print(f" Active File: {active_file}") - print() - - content = index_file.read_text(encoding="utf-8") - - if "@@@auto:current-status" not in content: - print("Error: Markers not found in index.md. Please ensure markers exist.", file=sys.stderr) - return False - - # Process sections - lines = content.splitlines() - new_lines = [] - - in_current_status = False - in_active_documents = False - in_session_history = False - header_written = False - - for line in lines: - if "@@@auto:current-status" in line: - new_lines.append(line) - in_current_status = True - new_lines.append(f"- **Active File**: `{active_file}`") - new_lines.append(f"- **Total Sessions**: {new_session}") - new_lines.append(f"- **Last Active**: {today}") - continue - - if "@@@/auto:current-status" in line: - in_current_status = False - new_lines.append(line) - continue - - if "@@@auto:active-documents" in line: - new_lines.append(line) - in_active_documents = True - new_lines.append("| File | Lines | Status |") - new_lines.append("|------|-------|--------|") - new_lines.append(files_table) - continue - - if "@@@/auto:active-documents" in line: - in_active_documents = False - new_lines.append(line) - continue - - if "@@@auto:session-history" in line: - new_lines.append(line) - in_session_history = True - header_written = False - continue - - if "@@@/auto:session-history" in line: - in_session_history = False - new_lines.append(line) - continue - - if in_current_status: - continue - - if in_active_documents: - continue - - if in_session_history: - # Migrate old 4/6-column headers to 5-column Branch-only history. - if re.match( - r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$", - line, - ): - new_lines.append("| # | Date | Title | Commits | Branch |") - continue - if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line): - new_lines.append("| # | Date | Title | Commits | Branch |") - continue - if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line): - new_lines.append("| # | Date | Title | Commits | Branch |") - continue - if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written: - new_lines.append("|---|------|-------|---------|--------|") - new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |") - header_written = True - continue - new_lines.append(line) - continue - - new_lines.append(line) - - index_file.write_text("\n".join(new_lines), encoding="utf-8") - print("[OK] Updated index.md successfully!") - return True - - -# ============================================================================= -# Main Function -# ============================================================================= - -def _auto_commit_workspace(repo_root: Path) -> None: - """Stage Trellis-owned workspace + task paths and commit. - - Path scope is restricted to specific products (journal files, index.md, - active task dirs, the archive subtree). We never `git add` the whole - `.trellis/` tree, and if `.gitignore` blocks the specific paths we - warn + skip — never retry with ``-f``. - - Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to - ``false``, this function returns immediately without touching git - (journal/index files are still written to disk by the caller). - """ - if not get_session_auto_commit(repo_root): - print( - "[OK] session_auto_commit: false — skipping git stage/commit.", - file=sys.stderr, - ) - return - - commit_msg = get_session_commit_message(repo_root) - paths = safe_trellis_paths_to_add(repo_root) - if not paths: - print("[OK] No workspace changes to commit.", file=sys.stderr) - return - - success, _, err = safe_git_add(paths, repo_root) - if not success: - if err and "ignored by" in err.lower(): - print_gitignore_warning(paths) - else: - print( - f"[WARN] git add failed: {err.strip() if err else 'unknown error'}", - file=sys.stderr, - ) - return - - # Check if there are staged changes for the paths we just staged. - rc, _, _ = run_git( - ["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root - ) - if rc == 0: - print("[OK] No workspace changes to commit.", file=sys.stderr) - return - - rc, _, commit_err = run_git(["commit", "-m", commit_msg], cwd=repo_root) - if rc == 0: - print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr) - else: - print( - f"[WARN] Auto-commit failed: {commit_err.strip()}", - file=sys.stderr, - ) - - -def add_session( - title: str, - commit: str = "-", - summary: str = "(Add summary)", - extra_content: str = "(Add details)", - auto_commit: bool = True, - package: str | None = None, - branch: str | None = None, -) -> int: - """Add a new session.""" - repo_root = get_repo_root() - ensure_developer(repo_root) - - developer = get_developer(repo_root) - if not developer: - print("Error: Developer not initialized", file=sys.stderr) - return 1 - - dev_dir = get_workspace_dir(repo_root) - if not dev_dir: - print("Error: Workspace directory not found", file=sys.stderr) - return 1 - - max_lines = get_max_journal_lines(repo_root) - - index_file = dev_dir / "index.md" - today = datetime.now().strftime("%Y-%m-%d") - - journal_file, current_num, current_lines = get_latest_journal_info(dev_dir) - current_session = get_current_session(index_file) - new_session = current_session + 1 - - session_content = generate_session_content( - new_session, title, commit, summary, extra_content, today, package, - branch, - ) - content_lines = len(session_content.splitlines()) - - print("========================================", file=sys.stderr) - print("ADD SESSION", file=sys.stderr) - print("========================================", file=sys.stderr) - print("", file=sys.stderr) - print(f"Session: {new_session}", file=sys.stderr) - print(f"Title: {title}", file=sys.stderr) - print(f"Commit: {commit}", file=sys.stderr) - print("", file=sys.stderr) - print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr) - print(f"Current lines: {current_lines}", file=sys.stderr) - print(f"New content lines: {content_lines}", file=sys.stderr) - print(f"Total after append: {current_lines + content_lines}", file=sys.stderr) - print("", file=sys.stderr) - - target_file = journal_file - target_num = current_num - - if current_lines + content_lines > max_lines: - target_num = current_num + 1 - print(f"[!] Exceeds {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr) - target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines) - print(f"Created: {target_file}", file=sys.stderr) - - # Append session content - if target_file: - with target_file.open("a", encoding="utf-8") as f: - f.write(session_content) - print(f"[OK] Appended session to {target_file.name}", file=sys.stderr) - - print("", file=sys.stderr) - - # Update index.md - active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md" - if not update_index( - index_file, - dev_dir, - title, - commit, - new_session, - active_file, - today, - branch, - ): - return 1 - - print("", file=sys.stderr) - print("========================================", file=sys.stderr) - print(f"[OK] Session {new_session} added successfully!", file=sys.stderr) - print("========================================", file=sys.stderr) - print("", file=sys.stderr) - print("Files updated:", file=sys.stderr) - print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr) - print(" - index.md", file=sys.stderr) - - # Auto-commit workspace changes - if auto_commit: - print("", file=sys.stderr) - _auto_commit_workspace(repo_root) - - return 0 - - -# ============================================================================= -# Main Entry -# ============================================================================= - -def main() -> int: - """CLI entry point.""" - parser = argparse.ArgumentParser( - description="Add a new session to journal file and update index.md" - ) - parser.add_argument("--title", required=True, help="Session title") - parser.add_argument("--commit", default="-", help="Comma-separated commit hashes") - parser.add_argument("--summary", default="(Add summary)", help="Brief summary") - parser.add_argument("--content-file", help="Path to file with detailed content") - parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)") - parser.add_argument("--branch", help="Branch name (auto-detected if omitted)") - parser.add_argument("--no-commit", action="store_true", - help="Skip auto-commit of workspace changes") - parser.add_argument("--stdin", action="store_true", - help="Read extra content from stdin (explicit opt-in)") - - args = parser.parse_args() - - extra_content = "(Add details)" - if args.content_file: - content_path = Path(args.content_file) - if content_path.is_file(): - extra_content = content_path.read_text(encoding="utf-8") - elif args.stdin: - extra_content = sys.stdin.read() - - # Load active task once — shared by package and branch resolution - repo_root = get_repo_root() - current = get_current_task(repo_root) - task_data = load_task(repo_root / current) if current else None - - package = args.package - if package: - # CLI source: fail-fast in monorepo, ignore in single-repo - if not is_monorepo(repo_root): - print("Warning: --package ignored in single-repo project", file=sys.stderr) - package = None - elif not validate_package(package, repo_root): - packages = get_packages(repo_root) - available = ", ".join(sorted(packages.keys())) if packages else "(none)" - print(f"Error: unknown package '{package}'. Available: {available}", file=sys.stderr) - return 1 - else: - # Inferred: active task's task.json.package → default_package → None - task_package = task_data.package if task_data else None - package = resolve_package(task_package, repo_root) - - # Resolve branch: CLI → task.json → git auto-detect → None - branch = args.branch - - if not branch: - if task_data and task_data.raw.get("branch"): - branch = task_data.raw["branch"] - else: - _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) - detected = branch_out.strip() - if detected: - branch = detected - - return add_session( - args.title, args.commit, args.summary, extra_content, - auto_commit=not args.no_commit, - package=package, - branch=branch, - ) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.trellis/scripts/common/__init__.py b/.trellis/scripts/common/__init__.py deleted file mode 100755 index 6d72360..0000000 --- a/.trellis/scripts/common/__init__.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Common utilities for Trellis workflow scripts. - -This module provides shared functionality used by other Trellis scripts. -""" - -import io -import sys - -# ============================================================================= -# Windows Encoding Fix (MUST be at top, before any other output) -# ============================================================================= -# On Windows, stdout defaults to the system code page (often GBK/CP936). -# This causes UnicodeEncodeError when printing non-ASCII characters. -# -# Any script that imports from common will automatically get this fix. -# ============================================================================= - - -def _configure_stream(stream: object) -> object: - """Configure a stream for UTF-8 encoding on Windows.""" - # Try reconfigure() first (Python 3.7+, more reliable) - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] - return stream - # Fallback: detach and rewrap with TextIOWrapper - elif hasattr(stream, "detach"): - return io.TextIOWrapper( - stream.detach(), # type: ignore[union-attr] - encoding="utf-8", - errors="replace", - ) - return stream - - -if sys.platform == "win32": - sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment] - sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment] - sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment] - - -def configure_encoding() -> None: - """ - Configure stdout/stderr/stdin for UTF-8 encoding on Windows. - - This is automatically called when importing from common, - but can be called manually for scripts that don't import common. - - Safe to call multiple times. - """ - global sys - if sys.platform == "win32": - sys.stdout = _configure_stream(sys.stdout) # type: ignore[assignment] - sys.stderr = _configure_stream(sys.stderr) # type: ignore[assignment] - sys.stdin = _configure_stream(sys.stdin) # type: ignore[assignment] - - -from .paths import ( - DIR_WORKFLOW, - DIR_WORKSPACE, - DIR_TASKS, - DIR_ARCHIVE, - DIR_SPEC, - DIR_SCRIPTS, - FILE_DEVELOPER, - FILE_CURRENT_TASK, - FILE_TASK_JSON, - FILE_JOURNAL_PREFIX, - get_repo_root, - get_developer, - check_developer, - get_tasks_dir, - get_workspace_dir, - get_active_journal_file, - count_lines, - get_current_task, - get_current_task_abs, - normalize_task_ref, - resolve_task_ref, - set_current_task, - clear_current_task, - has_current_task, - generate_task_date_prefix, -) - -from .active_task import ( - ActiveTask, - clear_active_task, - resolve_active_task, - resolve_context_key, - set_active_task, -) diff --git a/.trellis/scripts/common/active_task.py b/.trellis/scripts/common/active_task.py deleted file mode 100755 index e6597e8..0000000 --- a/.trellis/scripts/common/active_task.py +++ /dev/null @@ -1,626 +0,0 @@ -#!/usr/bin/env python3 -"""Session-scoped active task resolution. - -The user-facing concept is a single "active task". Trellis stores that pointer -per AI session/window under `.trellis/.runtime/sessions/`; without a stable -session key there is no active task. -""" - -from __future__ import annotations - -import hashlib -import json -import os -import re -import sys -import time -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -DIR_WORKFLOW = ".trellis" -DIR_TASKS = "tasks" -DIR_RUNTIME = ".runtime" -DIR_SESSIONS = "sessions" -DIR_CURSOR_SHELL = "cursor-shell" -CURSOR_SHELL_TICKET_TTL_SECONDS = 30 -TASK_SESSION_COMMANDS = {"start", "current", "finish"} - -_SESSION_KEYS = ("session_id", "sessionId", "sessionID") -_CONVERSATION_KEYS = ("conversation_id", "conversationId", "conversationID") -_TRANSCRIPT_KEYS = ("transcript_path", "transcriptPath", "transcript") -_NESTED_KEYS = ("input", "properties", "event", "hook_input", "hookInput") -_KNOWN_PLATFORMS = { - "claude", - "codex", - "cursor", - "opencode", - "gemini", - "droid", - "qoder", - "codebuddy", - "kiro", - "copilot", - "pi", -} - -_ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( - ("claude", ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID")), - ("codex", ("CODEX_SESSION_ID", "CODEX_THREAD_ID")), - ("cursor", ("CURSOR_SESSION_ID",)), - ("opencode", ("OPENCODE_SESSION_ID", "OPENCODE_SESSIONID", "OPENCODE_RUN_ID")), - ("gemini", ("GEMINI_SESSION_ID",)), - ("droid", ("FACTORY_SESSION_ID", "DROID_SESSION_ID")), - ("qoder", ("QODER_SESSION_ID",)), - ("codebuddy", ("CODEBUDDY_SESSION_ID",)), - ("kiro", ("KIRO_SESSION_ID",)), - ("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")), - ("pi", ("PI_SESSION_ID", "PI_SESSIONID")), -) -_ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( - ("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")), -) -_ENV_TRANSCRIPT_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = ( - ("claude", ("CLAUDE_TRANSCRIPT_PATH",)), - ("codex", ("CODEX_TRANSCRIPT_PATH",)), - ("cursor", ("CURSOR_TRANSCRIPT_PATH",)), - ("gemini", ("GEMINI_TRANSCRIPT_PATH",)), - ("droid", ("FACTORY_TRANSCRIPT_PATH", "DROID_TRANSCRIPT_PATH")), - ("qoder", ("QODER_TRANSCRIPT_PATH",)), - ("codebuddy", ("CODEBUDDY_TRANSCRIPT_PATH",)), -) -_ENV_PLATFORM_ALIASES = { - "claude-code": "claude", - "factory": "droid", - "factory-ai": "droid", - "github-copilot": "copilot", -} - - -@dataclass(frozen=True) -class ActiveTask: - """Resolved active task state.""" - - task_path: str | None - source_type: str - context_key: str | None = None - stale: bool = False - - @property - def source(self) -> str: - """Human-readable source label.""" - if self.source_type == "session" and self.context_key: - return f"session:{self.context_key}" - if self.source_type == "session-fallback" and self.context_key: - return f"session-fallback:{self.context_key}" - return self.source_type - - -def normalize_task_ref(task_ref: str) -> str: - """Normalize a task ref for stable storage and comparison.""" - normalized = task_ref.strip() - if not normalized: - return "" - - path_obj = Path(normalized) - if path_obj.is_absolute(): - return str(path_obj) - - normalized = normalized.replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - - if normalized.startswith(f"{DIR_TASKS}/"): - return f"{DIR_WORKFLOW}/{normalized}" - - return normalized - - -def resolve_task_ref(task_ref: str, repo_root: Path) -> Path | None: - """Resolve a task ref to an absolute task directory.""" - normalized = normalize_task_ref(task_ref) - if not normalized: - return None - - path_obj = Path(normalized) - if path_obj.is_absolute(): - return path_obj - - if normalized.startswith(f"{DIR_WORKFLOW}/"): - return repo_root / path_obj - - return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj - - -def _runtime_sessions_dir(repo_root: Path) -> Path: - return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_SESSIONS - - -def _sanitize_key(raw: str) -> str: - safe = re.sub(r"[^A-Za-z0-9._-]+", "_", raw.strip()) - safe = safe.strip("._-") - return safe[:160] if safe else "" - - -def _hash_value(raw: str) -> str: - return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24] - - -def _as_dict(value: Any) -> dict[str, Any] | None: - return value if isinstance(value, dict) else None - - -def _string_value(value: Any) -> str | None: - if isinstance(value, str): - stripped = value.strip() - return stripped or None - return None - - -def _lookup_string(data: dict[str, Any], keys: tuple[str, ...]) -> str | None: - for key in keys: - value = _string_value(data.get(key)) - if value: - return value - - for nested_key in _NESTED_KEYS: - nested = _as_dict(data.get(nested_key)) - if not nested: - continue - value = _lookup_string(nested, keys) - if value: - return value - - return None - - -def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None) -> str: - if platform: - return _sanitize_key(platform) or "session" - if platform_input: - for key in ("_trellis_platform", "trellis_platform", "platform", "source"): - value = _string_value(platform_input.get(key)) - if value: - return _sanitize_key(value) or "session" - if _string_value(platform_input.get("cursor_version")): - return "cursor" - return "session" - - -def _context_key(platform_name: str, kind: str, value: str) -> str: - if kind == "transcript": - return f"{platform_name}_transcript_{_hash_value(value)}" - safe_value = _sanitize_key(value) - if safe_value: - return f"{platform_name}_{safe_value}" - return f"{platform_name}_{_hash_value(value)}" - - -def _iter_env_keys( - env_keys: tuple[tuple[str, tuple[str, ...]], ...], - platform_name: str | None, -) -> tuple[tuple[str, tuple[str, ...]], ...]: - if not platform_name: - return env_keys - matched = tuple((name, keys) for name, keys in env_keys if name == platform_name) - return matched - - -def _env_platform_name(platform_name: str | None) -> str | None: - if not platform_name or platform_name == "session": - return None - return _ENV_PLATFORM_ALIASES.get(platform_name, platform_name) - - -def _lookup_env_context_key(platform_name: str | None) -> str | None: - """Resolve a context key from platform-provided environment variables. - - Hooks pass `TRELLIS_CONTEXT_ID` to subprocesses they launch, but an AI-run - shell command can only see session identity if the host platform exports it - in the command environment. These names are best-effort adapters; if none - are present, there is no session-scoped active task. - """ - env_platform_name = _env_platform_name(platform_name) - - for name, keys in _iter_env_keys(_ENV_SESSION_KEYS, env_platform_name): - for key in keys: - value = _string_value(os.environ.get(key)) - if value: - return _context_key(name, "session", value) - - for name, keys in _iter_env_keys(_ENV_CONVERSATION_KEYS, env_platform_name): - for key in keys: - value = _string_value(os.environ.get(key)) - if value: - return _context_key(name, "conversation", value) - - for name, keys in _iter_env_keys(_ENV_TRANSCRIPT_KEYS, env_platform_name): - for key in keys: - value = _string_value(os.environ.get(key)) - if value: - return _context_key(name, "transcript", value) - - return None - - -def _find_repo_root_from_cwd() -> Path | None: - current = Path.cwd().resolve() - while True: - if (current / DIR_WORKFLOW).is_dir(): - return current - if current == current.parent: - return None - current = current.parent - - -def _cursor_shell_ticket_dir(repo_root: Path) -> Path: - return repo_root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL - - -def _remove_file(path: Path) -> bool: - try: - path.unlink() - return True - except OSError: - return False - - -def _task_refs_match(left: str | None, right: str | None, repo_root: Path) -> bool: - if not left or not right: - return False - left_path = resolve_task_ref(left, repo_root) - right_path = resolve_task_ref(right, repo_root) - if left_path is not None and right_path is not None: - return left_path == right_path - return normalize_task_ref(left) == normalize_task_ref(right) - - -def _pending_ticket_matches_args(ticket: dict[str, Any], repo_root: Path) -> bool: - if Path(sys.argv[0]).name != "task.py": - return False - args = tuple(sys.argv[1:]) - if not args: - return False - - command_name = args[0] - if command_name not in TASK_SESSION_COMMANDS: - return False - - subcommands = ticket.get("subcommands") - if not isinstance(subcommands, list): - return False - - for subcommand in subcommands: - if not isinstance(subcommand, dict): - continue - if _string_value(subcommand.get("name")) != command_name: - continue - if command_name != "start": - return True - task_ref = args[1] if len(args) > 1 else None - if _task_refs_match(_string_value(subcommand.get("task_ref")), task_ref, repo_root): - return True - - return False - - -def _ticket_is_fresh(ticket: dict[str, Any], ticket_path: Path, now: float) -> bool: - expires_at = ticket.get("expires_at_epoch") - if isinstance(expires_at, (int, float)) and expires_at < now: - _remove_file(ticket_path) - return False - - created_at = ticket.get("created_at_epoch") - if isinstance(created_at, (int, float)): - if now - created_at <= CURSOR_SHELL_TICKET_TTL_SECONDS: - return True - _remove_file(ticket_path) - return False - return True - - -def _ticket_cwd_matches_repo(ticket: dict[str, Any], repo_root: Path) -> bool: - cwd = _string_value(ticket.get("cwd")) - if not cwd: - return True - try: - Path(cwd).resolve().relative_to(repo_root) - except ValueError: - return False - return True - - -def _matching_cursor_ticket_context_key( - ticket_path: Path, - repo_root: Path, - now: float, -) -> str | None: - ticket = _read_json(ticket_path) - if ticket is None or ticket.get("platform") != "cursor": - return None - if not _ticket_is_fresh(ticket, ticket_path, now): - return None - if not _ticket_cwd_matches_repo(ticket, repo_root): - return None - if not _pending_ticket_matches_args(ticket, repo_root): - return None - return _string_value(ticket.get("context_key")) - - -def _lookup_cursor_shell_ticket_context_key() -> str | None: - """Resolve Cursor conversation identity from a short-lived shell ticket. - - Cursor exposes `conversation_id` to `beforeShellExecution`, but does not - export it into the shell command environment. The Cursor hook writes a - short-lived ticket just before `task.py` runs. We accept a ticket only when - the current `task.py` subcommand matches and exactly one fresh context key - matches, which avoids cross-window pointer contamination. - """ - repo_root = _find_repo_root_from_cwd() - if repo_root is None: - return None - - ticket_dir = _cursor_shell_ticket_dir(repo_root) - if not ticket_dir.is_dir(): - return None - - now = time.time() - candidates: set[str] = set() - for ticket_path in ticket_dir.glob("*.json"): - context_key = _matching_cursor_ticket_context_key(ticket_path, repo_root, now) - if context_key: - candidates.add(context_key) - - if len(candidates) == 1: - return next(iter(candidates)) - return None - - -def resolve_context_key( - platform_input: dict[str, Any] | None = None, - platform: str | None = None, -) -> str | None: - """Resolve a stable session/window context key, if one is available. - - `TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI - scripts and subprocesses. It does not store the task itself. - """ - override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID")) - if override: - return _sanitize_key(override) or _hash_value(override) - - data = _as_dict(platform_input) - platform_name = _detect_platform(data, platform) if data or platform else None - - if data: - session_id = _lookup_string(data, _SESSION_KEYS) - if session_id: - return _context_key(platform_name or "session", "session", session_id) - - conversation_id = _lookup_string(data, _CONVERSATION_KEYS) - if conversation_id: - return _context_key(platform_name or "session", "conversation", conversation_id) - - transcript_path = _lookup_string(data, _TRANSCRIPT_KEYS) - if transcript_path: - return _context_key(platform_name or "session", "transcript", transcript_path) - - env_context_key = _lookup_env_context_key(platform_name) - if env_context_key: - return env_context_key - - if platform_name in (None, "session", "cursor"): - return _lookup_cursor_shell_ticket_context_key() - return None - - -def _read_json(path: Path) -> dict[str, Any] | None: - try: - data = json.loads(path.read_text(encoding="utf-8")) - except (FileNotFoundError, json.JSONDecodeError, OSError): - return None - return data if isinstance(data, dict) else None - - -def _write_json(path: Path, data: dict[str, Any]) -> bool: - try: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps(data, indent=2, ensure_ascii=False) + "\n", - encoding="utf-8", - ) - return True - except OSError: - return False - - -def _canonical_task_ref(task_path: str, repo_root: Path) -> str | None: - normalized = normalize_task_ref(task_path) - if not normalized: - return None - full_path = resolve_task_ref(normalized, repo_root) - if full_path is None or not full_path.is_dir(): - return None - try: - return full_path.relative_to(repo_root).as_posix() - except ValueError: - return str(full_path) - - -def _active_from_ref( - task_ref: str | None, - repo_root: Path, - source_type: str, - context_key: str | None = None, -) -> ActiveTask | None: - if not task_ref: - return None - resolved = resolve_task_ref(task_ref, repo_root) - stale = resolved is None or not resolved.is_dir() - return ActiveTask(task_ref, source_type, context_key, stale) - - -def _context_path(repo_root: Path, context_key: str) -> Path: - return _runtime_sessions_dir(repo_root) / f"{context_key}.json" - - -def resolve_active_task( - repo_root: Path, - platform_input: dict[str, Any] | None = None, - platform: str | None = None, -) -> ActiveTask: - """Resolve the active task from session runtime state only. - - A stale session task is returned as stale. Missing context identity or a - missing/empty session context falls back to single-session inference: if - exactly one session file exists in the runtime, return its task with - source_type="session-fallback" — covers class-2 platform sub-agents (codex, - copilot, gemini, qoder) that don't inherit the parent's session id. ≥2 - files or 0 files yield ActiveTask(None) — refuses to guess across windows. - """ - context_key = resolve_context_key(platform_input, platform) - if context_key: - context = _read_json(_context_path(repo_root, context_key)) or {} - task_ref = _string_value(context.get("current_task")) - active = _active_from_ref(task_ref, repo_root, "session", context_key) - if active: - return active - - fallback = _resolve_single_session_fallback(repo_root) - if fallback is not None: - return fallback - - return ActiveTask(None, "none", context_key) - - -def _resolve_single_session_fallback(repo_root: Path) -> ActiveTask | None: - """Return the task pointed at by the sole session file, if exactly one exists. - - Used when context-key resolution fails (typical for class-2 platform - sub-agents). Returns None if 0 or ≥2 session files are present — refuses - to pick across windows so 04-21's multi-session isolation contract holds. - """ - sessions_dir = _runtime_sessions_dir(repo_root) - if not sessions_dir.is_dir(): - return None - - session_files = sorted(sessions_dir.glob("*.json")) - if len(session_files) != 1: - return None - - session_file = session_files[0] - context = _read_json(session_file) or {} - task_ref = _string_value(context.get("current_task")) - if not task_ref: - return None - - fallback_key = session_file.stem - return _active_from_ref(task_ref, repo_root, "session-fallback", fallback_key) - - -def _utc_now() -> str: - return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") - - -def _context_metadata( - platform_input: dict[str, Any] | None, - platform: str | None, - context_key: str | None = None, -) -> dict[str, Any]: - data = _as_dict(platform_input) or {} - platform_name = _detect_platform(data, platform) - if platform_name == "session" and context_key: - prefix = context_key.split("_", 1)[0] - if prefix in _KNOWN_PLATFORMS: - platform_name = prefix - metadata: dict[str, Any] = { - "platform": platform_name, - "last_seen_at": _utc_now(), - } - for key in (*_SESSION_KEYS, *_CONVERSATION_KEYS, *_TRANSCRIPT_KEYS): - value = _lookup_string(data, (key,)) - if value: - metadata[key] = value - return metadata - - -def set_active_task( - task_path: str, - repo_root: Path, - platform_input: dict[str, Any] | None = None, - platform: str | None = None, -) -> ActiveTask | None: - """Set the active task in session scope. - - Returns None when no context key is available; callers should surface a - user-facing error that explains how to provide session identity. - """ - canonical = _canonical_task_ref(task_path, repo_root) - if canonical is None: - return None - - context_key = resolve_context_key(platform_input, platform) - if not context_key: - return None - - context_path = _context_path(repo_root, context_key) - context = _read_json(context_path) or {} - context.update(_context_metadata(platform_input, platform, context_key)) - context["current_task"] = canonical - context.setdefault("current_run", None) - if not _write_json(context_path, context): - return None - return ActiveTask(canonical, "session", context_key) - - -def clear_active_task( - repo_root: Path, - platform_input: dict[str, Any] | None = None, - platform: str | None = None, -) -> ActiveTask: - """Clear the active task by deleting the current session context file.""" - context_key = resolve_context_key(platform_input, platform) - if not context_key: - return ActiveTask(None, "none") - - previous = resolve_active_task(repo_root, platform_input, platform) - context_path = _context_path(repo_root, context_key) - if context_path.is_file(): - _remove_file(context_path) - return previous - - -def clear_task_from_sessions(task_path: str, repo_root: Path) -> int: - """Delete all session runtime files that point at a task.""" - target = _canonical_task_ref(task_path, repo_root) or normalize_task_ref(task_path) - if not target: - return 0 - - cleared = 0 - sessions_dir = _runtime_sessions_dir(repo_root) - if not sessions_dir.is_dir(): - return cleared - - for session_path in sessions_dir.glob("*.json"): - context = _read_json(session_path) or {} - current = _string_value(context.get("current_task")) - if not current: - continue - current_ref = _canonical_task_ref(current, repo_root) or normalize_task_ref(current) - if current_ref != target: - continue - if session_path.is_file() and _remove_file(session_path): - cleared += 1 - - return cleared - - -def get_current_task_source( - repo_root: Path, - platform_input: dict[str, Any] | None = None, - platform: str | None = None, -) -> tuple[str, str | None, str | None]: - """Return (`source_type`, `context_key`, `task_path`) for compatibility.""" - active = resolve_active_task(repo_root, platform_input, platform) - return active.source_type, active.context_key, active.task_path diff --git a/.trellis/scripts/common/cli_adapter.py b/.trellis/scripts/common/cli_adapter.py deleted file mode 100755 index b65f61a..0000000 --- a/.trellis/scripts/common/cli_adapter.py +++ /dev/null @@ -1,811 +0,0 @@ -""" -CLI Adapter for Multi-Platform Support. - -Abstracts differences between Claude Code, OpenCode, Cursor, iFlow, Codex, Kilo, Kiro Code, Gemini CLI, Antigravity, Windsurf, Qoder, CodeBuddy, GitHub Copilot, Factory Droid, and Pi Agent interfaces. - -Supported platforms: -- claude: Claude Code (default) -- opencode: OpenCode -- cursor: Cursor IDE -- iflow: iFlow CLI -- codex: Codex CLI (skills-based) -- kilo: Kilo CLI -- kiro: Kiro Code (skills-based) -- gemini: Gemini CLI -- antigravity: Antigravity (workflow-based) -- windsurf: Windsurf (workflow-based) -- qoder: Qoder -- codebuddy: CodeBuddy -- copilot: GitHub Copilot (VS Code) -- droid: Factory Droid (commands-based) -- pi: Pi Agent (extension-backed) - -Usage: - from common.cli_adapter import CLIAdapter - - adapter = CLIAdapter("opencode") - cmd = adapter.build_run_command( - agent="dispatch", - session_id="abc123", - prompt="Start the pipeline" - ) -""" - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import ClassVar, Literal - -Platform = Literal[ - "claude", - "opencode", - "cursor", - "iflow", - "codex", - "kilo", - "kiro", - "gemini", - "antigravity", - "windsurf", - "qoder", - "codebuddy", - "copilot", - "droid", - "pi", -] - - -@dataclass -class CLIAdapter: - """Adapter for different AI coding CLI tools.""" - - platform: Platform - - # ========================================================================= - # Agent Name Mapping - # ========================================================================= - - # OpenCode has built-in agents that cannot be overridden - # See: https://github.com/sst/opencode/issues/4271 - # Note: Class-level constant, not a dataclass field - _AGENT_NAME_MAP: ClassVar[dict[Platform, dict[str, str]]] = { - "claude": {}, # No mapping needed - "opencode": { - "plan": "trellis-plan", # 'plan' is built-in in OpenCode - }, - } - - def get_agent_name(self, agent: str) -> str: - """Get platform-specific agent name. - - Args: - agent: Original agent name (e.g., 'plan', 'dispatch') - - Returns: - Platform-specific agent name (e.g., 'trellis-plan' for OpenCode) - """ - mapping = self._AGENT_NAME_MAP.get(self.platform, {}) - return mapping.get(agent, agent) - - # ========================================================================= - # Agent Path - # ========================================================================= - - @property - def config_dir_name(self) -> str: - """Get platform-specific config directory name. - - Returns: - Directory name ('.claude', '.opencode', '.cursor', '.iflow', '.codex', '.kilocode', '.kiro', '.gemini', '.agent', '.windsurf', '.qoder', '.codebuddy', '.github/copilot', '.factory', or '.pi') - """ - if self.platform == "opencode": - return ".opencode" - elif self.platform == "cursor": - return ".cursor" - elif self.platform == "iflow": - return ".iflow" - elif self.platform == "codex": - return ".codex" - elif self.platform == "kilo": - return ".kilocode" - elif self.platform == "kiro": - return ".kiro" - elif self.platform == "gemini": - return ".gemini" - elif self.platform == "antigravity": - return ".agent" - elif self.platform == "windsurf": - return ".windsurf" - elif self.platform == "qoder": - return ".qoder" - elif self.platform == "codebuddy": - return ".codebuddy" - elif self.platform == "copilot": - return ".github/copilot" - elif self.platform == "droid": - return ".factory" - elif self.platform == "pi": - return ".pi" - else: - return ".claude" - - def get_config_dir(self, project_root: Path) -> Path: - """Get platform-specific config directory. - - Args: - project_root: Project root directory - - Returns: - Path to config directory (.claude, .opencode, .cursor, .iflow, .codex, .kilocode, .kiro, .gemini, .agent, .windsurf, .qoder, .codebuddy, .github/copilot, .factory, or .pi) - """ - return project_root / self.config_dir_name - - def get_agent_path(self, agent: str, project_root: Path) -> Path: - """Get path to agent definition file. - - Args: - agent: Agent name (original, before mapping) - project_root: Project root directory - - Returns: - Path to agent definition file (.md for most platforms, .toml for Codex) - """ - mapped_name = self.get_agent_name(agent) - if self.platform == "codex": - return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.toml" - return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.md" - - def get_commands_path(self, project_root: Path, *parts: str) -> Path: - """Get path to commands directory or specific command file. - - Args: - project_root: Project root directory - *parts: Additional path parts (e.g., 'trellis', 'finish-work.md') - - Returns: - Path to commands directory or file - - Note: - Cursor uses prefix naming: .cursor/commands/trellis-<name>.md - Antigravity uses workflow directory: .agent/workflows/<name>.md - Windsurf uses workflow directory: .windsurf/workflows/trellis-<name>.md - Copilot uses prompt files: .github/prompts/<name>.prompt.md - Pi uses prompt templates: .pi/prompts/trellis-<name>.md - Claude/OpenCode use subdirectory: .claude/commands/trellis/<name>.md - """ - if self.platform == "pi": - prompts_dir = self.get_config_dir(project_root) / "prompts" - if not parts: - return prompts_dir - if len(parts) >= 2 and parts[0] == "trellis": - filename = parts[-1] - if filename.endswith(".md"): - filename = filename[:-3] - return prompts_dir / f"trellis-{filename}.md" - return prompts_dir / Path(*parts) - - if self.platform == "windsurf": - workflow_dir = self.get_config_dir(project_root) / "workflows" - if not parts: - return workflow_dir - if len(parts) >= 2 and parts[0] == "trellis": - filename = parts[-1] - return workflow_dir / f"trellis-{filename}" - return workflow_dir / Path(*parts) - - if self.platform in ("antigravity", "kilo"): - workflow_dir = self.get_config_dir(project_root) / "workflows" - if not parts: - return workflow_dir - if len(parts) >= 2 and parts[0] == "trellis": - filename = parts[-1] - return workflow_dir / filename - return workflow_dir / Path(*parts) - - if self.platform == "copilot": - prompts_dir = project_root / ".github" / "prompts" - if not parts: - return prompts_dir - if len(parts) >= 2 and parts[0] == "trellis": - filename = parts[-1] - if filename.endswith(".md"): - filename = filename[:-3] - return prompts_dir / f"{filename}.prompt.md" - return prompts_dir / Path(*parts) - - if not parts: - return self.get_config_dir(project_root) / "commands" - - # Cursor uses prefix naming instead of subdirectory - if self.platform == "cursor" and len(parts) >= 2 and parts[0] == "trellis": - # Convert trellis/<name>.md to trellis-<name>.md - filename = parts[-1] - return ( - self.get_config_dir(project_root) / "commands" / f"trellis-{filename}" - ) - - return self.get_config_dir(project_root) / "commands" / Path(*parts) - - def get_trellis_command_path(self, name: str) -> str: - """Get relative path to a trellis command file. - - Args: - name: Command name without extension (e.g., 'finish-work', 'check') - - Returns: - Relative path string for use in JSONL entries - - Note: - Cursor: .cursor/commands/trellis-<name>.md - Codex: .agents/skills/trellis-<name>/SKILL.md - Kiro: .kiro/skills/trellis-<name>/SKILL.md - Gemini: .gemini/commands/trellis/<name>.toml - Antigravity: .agent/workflows/<name>.md - Windsurf: .windsurf/workflows/trellis-<name>.md - Pi: .pi/prompts/trellis-<name>.md - Others: .{platform}/commands/trellis/<name>.md - """ - if self.platform == "cursor": - return f".cursor/commands/trellis-{name}.md" - elif self.platform == "codex": - # 0.5.0-beta.0 renamed all skill dirs to add the `trellis-` prefix - # (see that release's manifest for the 60+ rename entries). - return f".agents/skills/trellis-{name}/SKILL.md" - elif self.platform == "kiro": - return f".kiro/skills/trellis-{name}/SKILL.md" - elif self.platform == "gemini": - return f".gemini/commands/trellis/{name}.toml" - elif self.platform == "antigravity": - return f".agent/workflows/{name}.md" - elif self.platform == "windsurf": - return f".windsurf/workflows/trellis-{name}.md" - elif self.platform == "kilo": - return f".kilocode/workflows/{name}.md" - elif self.platform == "copilot": - return f".github/prompts/{name}.prompt.md" - elif self.platform == "droid": - return f".factory/commands/trellis/{name}.md" - elif self.platform == "pi": - return f".pi/prompts/trellis-{name}.md" - else: - return f"{self.config_dir_name}/commands/trellis/{name}.md" - - # ========================================================================= - # Environment Variables - # ========================================================================= - - def get_non_interactive_env(self) -> dict[str, str]: - """Get environment variables for non-interactive mode. - - Returns: - Dict of environment variables to set - """ - if self.platform == "opencode": - return {"OPENCODE_NON_INTERACTIVE": "1"} - elif self.platform == "iflow": - return {"IFLOW_NON_INTERACTIVE": "1"} - elif self.platform == "codex": - return {"CODEX_NON_INTERACTIVE": "1"} - elif self.platform == "kiro": - return {"KIRO_NON_INTERACTIVE": "1"} - elif self.platform == "gemini": - return {} # Gemini CLI doesn't have a non-interactive env var - elif self.platform == "antigravity": - return {} - elif self.platform == "windsurf": - return {} - elif self.platform == "qoder": - return {} - elif self.platform == "codebuddy": - return {} - elif self.platform == "copilot": - return {} - elif self.platform == "droid": - return {} - elif self.platform == "pi": - return {} - else: - return {"CLAUDE_NON_INTERACTIVE": "1"} - - # ========================================================================= - # CLI Command Building - # ========================================================================= - - def build_run_command( - self, - agent: str, - prompt: str, - session_id: str | None = None, - skip_permissions: bool = True, - verbose: bool = True, - json_output: bool = True, - ) -> list[str]: - """Build CLI command for running an agent. - - Args: - agent: Agent name (will be mapped if needed) - prompt: Prompt to send to the agent - session_id: Optional session ID (Claude Code only for creation) - skip_permissions: Whether to skip permission prompts - verbose: Whether to enable verbose output - json_output: Whether to use JSON output format - - Returns: - List of command arguments - """ - mapped_agent = self.get_agent_name(agent) - - if self.platform == "opencode": - cmd = ["opencode", "run"] - cmd.extend(["--agent", mapped_agent]) - - # Note: OpenCode 'run' mode is non-interactive by default - # No equivalent to Claude Code's --dangerously-skip-permissions - # See: https://github.com/anomalyco/opencode/issues/9070 - - if json_output: - cmd.extend(["--format", "json"]) - - if verbose: - cmd.extend(["--log-level", "DEBUG", "--print-logs"]) - - # Note: OpenCode doesn't support --session-id on creation - # Session ID must be extracted from logs after startup - - cmd.append(prompt) - - elif self.platform == "iflow": - cmd = ["iflow", "-y", "-p"] - cmd.append(f"${mapped_agent} {prompt}") - elif self.platform == "codex": - cmd = ["codex", "exec"] - cmd.append(prompt) - elif self.platform == "kiro": - cmd = ["kiro", "run", prompt] - elif self.platform == "gemini": - cmd = ["gemini"] - cmd.append(prompt) - elif self.platform == "antigravity": - raise ValueError( - "Antigravity workflows are UI slash commands; CLI agent run is not supported." - ) - elif self.platform == "windsurf": - raise ValueError( - "Windsurf workflows are UI slash commands; CLI agent run is not supported." - ) - elif self.platform == "qoder": - cmd = ["qodercli", "-p", prompt] - elif self.platform == "codebuddy": - raise ValueError( - "CodeBuddy does not support non-interactive mode (no CLI agent)" - ) - elif self.platform == "copilot": - raise ValueError( - "GitHub Copilot is IDE-only; CLI agent run is not supported." - ) - elif self.platform == "droid": - raise ValueError( - "Factory Droid CLI agent run is not yet supported." - ) - elif self.platform == "pi": - cmd = ["pi", "-p", prompt] - - else: # claude - cmd = ["claude", "-p"] - cmd.extend(["--agent", mapped_agent]) - - if session_id: - cmd.extend(["--session-id", session_id]) - - if skip_permissions: - cmd.append("--dangerously-skip-permissions") - - if json_output: - cmd.extend(["--output-format", "stream-json"]) - - if verbose: - cmd.append("--verbose") - - cmd.append(prompt) - - return cmd - - def build_resume_command(self, session_id: str) -> list[str]: - """Build CLI command for resuming a session. - - Args: - session_id: Session ID to resume (ignored for iFlow) - - Returns: - List of command arguments - """ - if self.platform == "opencode": - return ["opencode", "run", "--session", session_id] - elif self.platform == "iflow": - # iFlow uses -c to continue most recent conversation - # session_id is ignored as iFlow doesn't support session IDs - return ["iflow", "-c"] - elif self.platform == "codex": - return ["codex", "resume", session_id] - elif self.platform == "kiro": - return ["kiro", "resume", session_id] - elif self.platform == "gemini": - return ["gemini", "--resume", session_id] - elif self.platform == "antigravity": - raise ValueError( - "Antigravity workflows are UI slash commands; CLI resume is not supported." - ) - elif self.platform == "windsurf": - raise ValueError( - "Windsurf workflows are UI slash commands; CLI resume is not supported." - ) - elif self.platform == "qoder": - return ["qodercli", "--resume", session_id] - elif self.platform == "codebuddy": - raise ValueError( - "CodeBuddy does not support non-interactive mode (no CLI agent)" - ) - elif self.platform == "copilot": - raise ValueError( - "GitHub Copilot is IDE-only; CLI resume is not supported." - ) - elif self.platform == "droid": - raise ValueError( - "Factory Droid CLI resume is not yet supported." - ) - elif self.platform == "pi": - return ["pi", "-c", session_id] - else: - return ["claude", "--resume", session_id] - - def get_resume_command_str(self, session_id: str, cwd: str | None = None) -> str: - """Get human-readable resume command string. - - Args: - session_id: Session ID to resume - cwd: Optional working directory to cd into - - Returns: - Command string for display - """ - cmd = self.build_resume_command(session_id) - cmd_str = " ".join(cmd) - - if cwd: - return f"cd {cwd} && {cmd_str}" - return cmd_str - - # ========================================================================= - # Platform Detection Helpers - # ========================================================================= - - @property - def is_opencode(self) -> bool: - """Check if platform is OpenCode.""" - return self.platform == "opencode" - - @property - def is_claude(self) -> bool: - """Check if platform is Claude Code.""" - return self.platform == "claude" - - @property - def is_cursor(self) -> bool: - """Check if platform is Cursor.""" - return self.platform == "cursor" - - @property - def is_iflow(self) -> bool: - """Check if platform is iFlow CLI.""" - return self.platform == "iflow" - - @property - def cli_name(self) -> str: - """Get CLI executable name. - - Note: Cursor doesn't have a CLI tool, returns None-like value. - """ - if self.is_opencode: - return "opencode" - elif self.is_cursor: - return "cursor" # Note: Cursor is IDE-only, no CLI - elif self.platform == "iflow": - return "iflow" - elif self.platform == "kiro": - return "kiro" - elif self.platform == "gemini": - return "gemini" - elif self.platform == "antigravity": - return "agy" - elif self.platform == "windsurf": - return "windsurf" - elif self.platform == "qoder": - return "qodercli" - elif self.platform == "codebuddy": - return "codebuddy" - elif self.platform == "copilot": - return "copilot" - elif self.platform == "droid": - return "droid" - elif self.platform == "pi": - return "pi" - else: - return "claude" - - @property - def supports_cli_agents(self) -> bool: - """Check if platform supports running agents via CLI. - - Claude Code, OpenCode, iFlow, and Codex support CLI agent execution. - Cursor is IDE-only and doesn't support CLI agents. - """ - return self.platform in ("claude", "opencode", "iflow", "codex", "pi") - - @property - def requires_agent_definition_file(self) -> bool: - """Check if platform requires an agent definition file (.md/.toml) to run. - - Claude Code, OpenCode, iFlow: require agent .md files (--agent flag). - Codex: auto-discovers agents from .codex/agents/*.toml, no --agent flag. - """ - return self.platform in ("claude", "opencode", "iflow") - - # ========================================================================= - # Session ID Handling - # ========================================================================= - - @property - def supports_session_id_on_create(self) -> bool: - """Check if platform supports specifying session ID on creation. - - Claude Code: Yes (--session-id) - OpenCode: No (auto-generated, extract from logs) - iFlow: No (no session ID support) - """ - return self.platform == "claude" - - def extract_session_id_from_log(self, log_content: str) -> str | None: - """Extract session ID from log output (OpenCode only). - - OpenCode generates session IDs in format: ses_xxx - - Args: - log_content: Log file content - - Returns: - Session ID if found, None otherwise - """ - import re - - # OpenCode session ID pattern - match = re.search(r"ses_[a-zA-Z0-9]+", log_content) - if match: - return match.group(0) - return None - - -# ============================================================================= -# Factory Function -# ============================================================================= - - -def get_cli_adapter(platform: str = "claude") -> CLIAdapter: - """Get CLI adapter for the specified platform. - - Args: - platform: Platform name ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi') - - Returns: - CLIAdapter instance - - Raises: - ValueError: If platform is not supported - """ - if platform not in ( - "claude", - "opencode", - "cursor", - "iflow", - "codex", - "kilo", - "kiro", - "gemini", - "antigravity", - "windsurf", - "qoder", - "codebuddy", - "copilot", - "droid", - "pi", - ): - raise ValueError( - f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', or 'pi')" - ) - - return CLIAdapter(platform=platform) # type: ignore - - -_ALL_PLATFORM_CONFIG_DIRS = ( - ".claude", - ".cursor", - ".iflow", - ".opencode", - ".codex", - ".kilocode", - ".kiro", - ".gemini", - ".agent", - ".windsurf", - ".qoder", - ".codebuddy", - ".github/copilot", - ".factory", - ".pi", -) -"""Platform-specific config directory names used by detect_platform exclusion -checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform -layer (written by Codex, also consumed by Amp/Cline/Warp/etc. via the -agentskills.io standard), not a single-platform signal. Its presence must not -block detection of Kiro, Antigravity, Windsurf, or other platforms.""" - - -def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool: - """Check if any platform config dir exists besides those in *exclude*.""" - return any( - (project_root / d).is_dir() - for d in _ALL_PLATFORM_CONFIG_DIRS - if d not in exclude - ) - - -def detect_platform(project_root: Path) -> Platform: - """Auto-detect platform based on existing config directories. - - Detection order: - 1. TRELLIS_PLATFORM environment variable (if set) - 2. .opencode directory exists → opencode - 3. .iflow directory exists → iflow - 4. .cursor directory exists (without .claude) → cursor - 5. .codex exists and no other platform dirs → codex - 6. .kilocode directory exists → kilo - 7. .kiro/skills exists and no other platform dirs → kiro - 8. .gemini directory exists → gemini - 9. .agent/workflows exists and no other platform dirs → antigravity - 10. .windsurf/workflows exists and no other platform dirs → windsurf - 11. .codebuddy directory exists → codebuddy - 12. .qoder directory exists → qoder - 13. .pi directory exists → pi - 14. Default → claude - - Args: - project_root: Project root directory - - Returns: - Detected platform ('claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'windsurf', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', or default 'claude') - """ - import os - - # Check environment variable first - env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower() - if env_platform in ( - "claude", - "opencode", - "cursor", - "iflow", - "codex", - "kilo", - "kiro", - "gemini", - "antigravity", - "windsurf", - "qoder", - "codebuddy", - "copilot", - "droid", - "pi", - ): - return env_platform # type: ignore - - # Check for .opencode directory (OpenCode-specific) - if (project_root / ".opencode").is_dir(): - return "opencode" - - # Check for .iflow directory (iFlow-specific) - if (project_root / ".iflow").is_dir(): - return "iflow" - - # Check for .cursor directory (Cursor-specific) - # Only detect as cursor if .claude doesn't exist (to avoid confusion) - if (project_root / ".cursor").is_dir() and not (project_root / ".claude").is_dir(): - return "cursor" - - # Check for .gemini directory (Gemini CLI-specific) - if (project_root / ".gemini").is_dir(): - return "gemini" - - # Check for .codex directory (Codex-specific) - # .agents/skills/ alone does NOT trigger codex detection (it's a shared standard) - if (project_root / ".codex").is_dir() and not _has_other_platform_dir( - project_root, {".codex", ".agents"} - ): - return "codex" - - # Check for .kilocode directory (Kilo-specific) - if (project_root / ".kilocode").is_dir(): - return "kilo" - - # Check for Kiro skills directory only when no other platform config exists - if (project_root / ".kiro" / "skills").is_dir() and not _has_other_platform_dir( - project_root, {".kiro"} - ): - return "kiro" - - # Check for Antigravity workflow directory only when no other platform config exists - if ( - project_root / ".agent" / "workflows" - ).is_dir() and not _has_other_platform_dir( - project_root, {".agent", ".gemini"} - ): - return "antigravity" - - # Check for Windsurf workflow directory only when no other platform config exists - if ( - project_root / ".windsurf" / "workflows" - ).is_dir() and not _has_other_platform_dir( - project_root, {".windsurf"} - ): - return "windsurf" - - # Check for .codebuddy directory (CodeBuddy-specific) - if (project_root / ".codebuddy").is_dir(): - return "codebuddy" - - # Check for .qoder directory (Qoder-specific) - if (project_root / ".qoder").is_dir(): - return "qoder" - - # Check for .github/copilot directory (GitHub Copilot-specific) - if (project_root / ".github" / "copilot").is_dir(): - return "copilot" - - # Check for .factory directory (Factory Droid-specific) - if (project_root / ".factory").is_dir(): - return "droid" - - # Check for .pi directory (Pi Agent-specific) - if (project_root / ".pi").is_dir(): - return "pi" - - # Fallback: checkout only has the Codex shared-skills layer - # (.agents/skills/trellis-* dirs) and no explicit platform config dir. - # Happens on fresh clones where .codex/ is gitignored/absent but the - # shared skills were committed to git. Must guard against the case - # where .claude/ or any other platform dir also exists — .agents/skills/ - # can legitimately coexist with any platform as a shared consumption - # layer for Amp/Cline/Warp/etc. - agents_skills = project_root / ".agents" / "skills" - if agents_skills.is_dir() and not _has_other_platform_dir( - project_root, set() - ): - try: - for entry in agents_skills.iterdir(): - if entry.is_dir() and entry.name.startswith("trellis-"): - return "codex" - except OSError: - pass - - return "claude" - - -def get_cli_adapter_auto(project_root: Path) -> CLIAdapter: - """Get CLI adapter with auto-detected platform. - - Args: - project_root: Project root directory - - Returns: - CLIAdapter instance for detected platform - """ - platform = detect_platform(project_root) - return CLIAdapter(platform=platform) diff --git a/.trellis/scripts/common/config.py b/.trellis/scripts/common/config.py deleted file mode 100755 index 93df643..0000000 --- a/.trellis/scripts/common/config.py +++ /dev/null @@ -1,445 +0,0 @@ -#!/usr/bin/env python3 -""" -Trellis configuration reader. - -Reads settings from .trellis/config.yaml with sensible defaults. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -from .paths import DIR_WORKFLOW, get_repo_root - - -# ============================================================================= -# YAML Simple Parser (no dependencies) -# ============================================================================= - - -def _unquote(s: str) -> str: - """Remove exactly one layer of matching surrounding quotes. - - Unlike str.strip('"'), this only removes the outermost pair, - preserving any nested quotes inside the value. - - Examples: - _unquote('"hello"') -> 'hello' - _unquote("'hello'") -> 'hello' - _unquote('"echo \\'hi\\'"') -> "echo 'hi'" - _unquote('hello') -> 'hello' - _unquote('"hello\\'') -> '"hello\\'' (mismatched, unchanged) - """ - if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"): - return s[1:-1] - return s - - -def _strip_inline_comment(value: str) -> str: - """Strip ` # …` inline comments while preserving `#` inside quoted strings. - - YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token - is part of the value. Quoted strings are immune. - - Mirrors :func:`common.trellis_config._strip_inline_comment` so both - parsers handle ``key: value # comment`` identically. - """ - in_quote: str | None = None - for idx, ch in enumerate(value): - if in_quote: - if ch == in_quote: - in_quote = None - continue - if ch in ('"', "'"): - in_quote = ch - continue - if ch == "#" and (idx == 0 or value[idx - 1].isspace()): - return value[:idx] - return value - - -def parse_simple_yaml(content: str) -> dict: - """Parse simple YAML with nested dict support (no dependencies). - - Supports: - - key: value (string) - - key: (followed by list items) - - item1 - - item2 - - key: (followed by nested dict) - nested_key: value - nested_key2: - - item - - Uses indentation to detect nesting (2+ spaces deeper = child). - - Args: - content: YAML content string. - - Returns: - Parsed dict (values can be str, list[str], or dict). - """ - lines = content.splitlines() - result: dict = {} - _parse_yaml_block(lines, 0, 0, result) - return result - - -def _parse_yaml_block( - lines: list[str], start: int, min_indent: int, target: dict -) -> int: - """Parse a YAML block into target dict, returning next line index.""" - i = start - current_list: list | None = None - - while i < len(lines): - line = lines[i] - stripped = line.strip() - - # Skip empty lines and comments - if not stripped or stripped.startswith("#"): - i += 1 - continue - - # Calculate indentation - indent = len(line) - len(line.lstrip()) - - # If dedented past our block, we're done - if indent < min_indent: - break - - if stripped.startswith("- "): - if current_list is not None: - current_list.append(_unquote(stripped[2:].strip())) - i += 1 - elif ":" in stripped: - key, _, value = stripped.partition(":") - key = key.strip() - value = _strip_inline_comment(value).strip() - value = _unquote(value) - current_list = None - - if value: - # key: value - target[key] = value - i += 1 - else: - # key: (no value) — peek ahead to determine list vs nested dict - next_i, next_line = _next_content_line(lines, i + 1) - if next_i >= len(lines): - target[key] = {} - i = next_i - elif next_line.strip().startswith("- "): - # It's a list - current_list = [] - target[key] = current_list - i += 1 - else: - next_indent = len(next_line) - len(next_line.lstrip()) - if next_indent > indent: - # It's a nested dict - nested: dict = {} - target[key] = nested - i = _parse_yaml_block(lines, i + 1, next_indent, nested) - else: - # Empty value, same or less indent follows - target[key] = {} - i += 1 - else: - i += 1 - - return i - - -def _next_content_line(lines: list[str], start: int) -> tuple[int, str]: - """Find the next non-empty, non-comment line.""" - i = start - while i < len(lines): - stripped = lines[i].strip() - if stripped and not stripped.startswith("#"): - return i, lines[i] - i += 1 - return i, "" - - -# Defaults -DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal" -DEFAULT_MAX_JOURNAL_LINES = 2000 -DEFAULT_SESSION_AUTO_COMMIT = True - -CONFIG_FILE = "config.yaml" - - -def _is_true_config_value(value: object) -> bool: - """Return True when a config value represents an enabled flag.""" - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() == "true" - return False - - -def _get_config_path(repo_root: Path | None = None) -> Path: - """Get path to config.yaml.""" - root = repo_root or get_repo_root() - return root / DIR_WORKFLOW / CONFIG_FILE - - -def _load_config(repo_root: Path | None = None) -> dict: - """Load and parse config.yaml. Returns empty dict on any error.""" - config_file = _get_config_path(repo_root) - try: - content = config_file.read_text(encoding="utf-8") - return parse_simple_yaml(content) - except (OSError, IOError): - return {} - - -def get_session_commit_message(repo_root: Path | None = None) -> str: - """Get the commit message for auto-committing session records.""" - config = _load_config(repo_root) - return config.get("session_commit_message", DEFAULT_SESSION_COMMIT_MESSAGE) - - -def get_max_journal_lines(repo_root: Path | None = None) -> int: - """Get the maximum lines per journal file.""" - config = _load_config(repo_root) - value = config.get("max_journal_lines", DEFAULT_MAX_JOURNAL_LINES) - try: - return int(value) - except (ValueError, TypeError): - return DEFAULT_MAX_JOURNAL_LINES - - -def get_session_auto_commit(repo_root: Path | None = None) -> bool: - """Whether scripts should auto-stage + auto-commit session/task changes. - - Governs both ``add_session.py:_auto_commit_workspace`` and - ``task_store.py:_auto_commit_archive``. - - Default: ``True`` (existing behavior — auto-stage + auto-commit). - Set ``session_auto_commit: false`` in ``.trellis/config.yaml`` to skip - auto-staging entirely; the journal/archive files are still written to - disk, but the user manages ``git add`` / ``git commit`` themselves. - - Accepts native YAML booleans (``true`` / ``false``) and the string - aliases ``true / false / yes / no / 1 / 0 / on / off`` (case-insensitive). - Invalid values fall back to ``True`` with a stderr warning. - """ - config = _load_config(repo_root) - raw = config.get("session_auto_commit", DEFAULT_SESSION_AUTO_COMMIT) - if isinstance(raw, bool): - return raw - s = str(raw).strip().lower() - if s in ("true", "yes", "1", "on"): - return True - if s in ("false", "no", "0", "off"): - return False - print( - f"[WARN] invalid session_auto_commit value: {raw!r}; using true (default)", - file=sys.stderr, - ) - return DEFAULT_SESSION_AUTO_COMMIT - - -def get_hooks(event: str, repo_root: Path | None = None) -> list[str]: - """Get hook commands for a lifecycle event. - - Args: - event: Event name (e.g. "after_create", "after_archive"). - repo_root: Repository root path. - - Returns: - List of shell commands to execute, empty if none configured. - """ - config = _load_config(repo_root) - hooks = config.get("hooks") - if not isinstance(hooks, dict): - return [] - commands = hooks.get(event) - if isinstance(commands, list): - return [str(c) for c in commands] - return [] - - -# ============================================================================= -# Monorepo / Packages -# ============================================================================= - - -def get_packages(repo_root: Path | None = None) -> dict[str, dict] | None: - """Get monorepo package declarations. - - Returns: - Dict mapping package name to its config (path, type, etc.), - or None if not configured (single-repo mode). - - Example return: - {"cli": {"path": "packages/cli"}, "docs-site": {"path": "docs-site", "type": "submodule"}} - """ - config = _load_config(repo_root) - packages = config.get("packages") - if not isinstance(packages, dict): - return None - # Ensure each value is a dict (filter out scalar entries) - filtered = {k: v for k, v in packages.items() if isinstance(v, dict)} - if not filtered: - return None - return filtered - - -def get_default_package(repo_root: Path | None = None) -> str | None: - """Get the default package name from config. - - Returns: - Package name string, or None if not configured. - """ - config = _load_config(repo_root) - value = config.get("default_package") - return str(value) if value else None - - -def get_submodule_packages(repo_root: Path | None = None) -> dict[str, str]: - """Get packages that are git submodules. - - Returns: - Dict mapping package name to its path for submodule-type packages. - Empty dict if none configured. - - Example return: - {"docs-site": "docs-site"} - """ - packages = get_packages(repo_root) - if packages is None: - return {} - return { - name: cfg.get("path", name) - for name, cfg in packages.items() - if cfg.get("type") == "submodule" - } - - -def get_git_packages(repo_root: Path | None = None) -> dict[str, str]: - """Get packages that have their own independent git repository. - - These are sub-directories with their own .git (not submodules), - marked with ``git: true`` in config.yaml. - - Returns: - Dict mapping package name to its path for git-repo packages. - Empty dict if none configured. - - Example config:: - - packages: - backend: - path: iqs - git: true - - Example return:: - - {"backend": "iqs"} - """ - packages = get_packages(repo_root) - if packages is None: - return {} - return { - name: cfg.get("path", name) - for name, cfg in packages.items() - if _is_true_config_value(cfg.get("git")) - } - - -def is_monorepo(repo_root: Path | None = None) -> bool: - """Check if the project is configured as a monorepo (has packages in config).""" - return get_packages(repo_root) is not None - - -def get_spec_base(package: str | None = None, repo_root: Path | None = None) -> str: - """Get the spec directory base path relative to .trellis/. - - Single-repo: returns "spec" - Monorepo with package: returns "spec/<package>" - Monorepo without package: returns "spec" (caller should specify package) - """ - if package and is_monorepo(repo_root): - return f"spec/{package}" - return "spec" - - -def validate_package(package: str, repo_root: Path | None = None) -> bool: - """Check if a package name is valid in this project. - - Single-repo (no packages configured): always returns True. - Monorepo: returns True only if package exists in config.yaml packages. - """ - packages = get_packages(repo_root) - if packages is None: - return True # Single-repo, no validation needed - return package in packages - - -def resolve_package( - task_package: str | None = None, - repo_root: Path | None = None, -) -> str | None: - """Resolve package from inferred sources with validation. - - Checks in order: task_package → default_package. - Invalid inferred values print a warning to stderr and are skipped. - - Returns: - Resolved package name, or None if no valid package found. - - Note: - CLI --package should be validated separately by the caller - (fail-fast with available packages list on error). - """ - packages = get_packages(repo_root) - if packages is None: - return None # Single-repo, no package needed - - # Try task_package (guard against non-string values from malformed JSON) - if task_package and isinstance(task_package, str): - if task_package in packages: - return task_package - print( - f"Warning: task.json package '{task_package}' not found in config, skipping", - file=sys.stderr, - ) - - # Try default_package - default = get_default_package(repo_root) - if default: - if default in packages: - return default - print( - f"Warning: default_package '{default}' not found in config, skipping", - file=sys.stderr, - ) - - return None - - -def get_spec_scope(repo_root: Path | None = None) -> list[str] | str | None: - """Get session.spec_scope configuration. - - Returns: - list[str]: Package names to include in spec scanning. - str: "active_task" to use current task's package. - None: No scope configured (scan all packages). - """ - config = _load_config(repo_root) - session = config.get("session") - if not isinstance(session, dict): - return None - - scope = session.get("spec_scope") - if scope is None: - return None - if isinstance(scope, str): - return scope # e.g. "active_task" - if isinstance(scope, list): - return [str(s) for s in scope] - return None diff --git a/.trellis/scripts/common/developer.py b/.trellis/scripts/common/developer.py deleted file mode 100755 index c203a31..0000000 --- a/.trellis/scripts/common/developer.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -""" -Developer management utilities. - -Provides: - init_developer - Initialize developer - ensure_developer - Ensure developer is initialized (exit if not) - show_developer_info - Show developer information -""" - -from __future__ import annotations - -import sys -from datetime import datetime -from pathlib import Path - -from .paths import ( - DIR_WORKFLOW, - DIR_WORKSPACE, - DIR_TASKS, - FILE_DEVELOPER, - FILE_JOURNAL_PREFIX, - get_repo_root, - get_developer, - check_developer, -) - - -# ============================================================================= -# Developer Initialization -# ============================================================================= - -def init_developer(name: str, repo_root: Path | None = None) -> bool: - """Initialize developer. - - Creates: - - .trellis/.developer file with developer info - - .trellis/workspace/<name>/ directory structure - - Initial journal file and index.md - - Args: - name: Developer name. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True on success, False on error. - """ - if not name: - print("Error: developer name is required", file=sys.stderr) - return False - - if repo_root is None: - repo_root = get_repo_root() - - dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER - workspace_dir = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / name - - # Create .developer file - initialized_at = datetime.now().isoformat() - try: - dev_file.write_text( - f"name={name}\ninitialized_at={initialized_at}\n", - encoding="utf-8" - ) - except (OSError, IOError) as e: - print(f"Error: Failed to create .developer file: {e}", file=sys.stderr) - return False - - # Create workspace directory structure - try: - workspace_dir.mkdir(parents=True, exist_ok=True) - except (OSError, IOError) as e: - print(f"Error: Failed to create workspace directory: {e}", file=sys.stderr) - return False - - # Create initial journal file - journal_file = workspace_dir / f"{FILE_JOURNAL_PREFIX}1.md" - if not journal_file.exists(): - today = datetime.now().strftime("%Y-%m-%d") - journal_content = f"""# Journal - {name} (Part 1) - -> AI development session journal -> Started: {today} - ---- - -""" - try: - journal_file.write_text(journal_content, encoding="utf-8") - except (OSError, IOError) as e: - print(f"Error: Failed to create journal file: {e}", file=sys.stderr) - return False - - # Create index.md with markers for auto-update - index_file = workspace_dir / "index.md" - if not index_file.exists(): - index_content = f"""# Workspace Index - {name} - -> Journal tracking for AI development sessions. - ---- - -## Current Status - -<!-- @@@auto:current-status --> -- **Active File**: `journal-1.md` -- **Total Sessions**: 0 -- **Last Active**: - -<!-- @@@/auto:current-status --> - ---- - -## Active Documents - -<!-- @@@auto:active-documents --> -| File | Lines | Status | -|------|-------|--------| -| `journal-1.md` | ~0 | Active | -<!-- @@@/auto:active-documents --> - ---- - -## Session History - -<!-- @@@auto:session-history --> -| # | Date | Title | Commits | Branch | -|---|------|-------|---------|--------| -<!-- @@@/auto:session-history --> - ---- - -## Notes - -- Sessions are appended to journal files -- New journal file created when current exceeds 2000 lines -- Use `add_session.py` to record sessions -""" - try: - index_file.write_text(index_content, encoding="utf-8") - except (OSError, IOError) as e: - print(f"Error: Failed to create index.md: {e}", file=sys.stderr) - return False - - print(f"Developer initialized: {name}") - print(f" .developer file: {dev_file}") - print(f" Workspace dir: {workspace_dir}") - - return True - - -def ensure_developer(repo_root: Path | None = None) -> None: - """Ensure developer is initialized, exit if not. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - """ - if repo_root is None: - repo_root = get_repo_root() - - if not check_developer(repo_root): - print("Error: Developer not initialized.", file=sys.stderr) - print(f"Run: python3 ./{DIR_WORKFLOW}/scripts/init_developer.py <your-name>", file=sys.stderr) - sys.exit(1) - - -def show_developer_info(repo_root: Path | None = None) -> None: - """Show developer information. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - """ - if repo_root is None: - repo_root = get_repo_root() - - developer = get_developer(repo_root) - - if not developer: - print("Developer: (not initialized)") - else: - print(f"Developer: {developer}") - print(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/") - print(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/") - - -# ============================================================================= -# Main Entry (for testing) -# ============================================================================= - -if __name__ == "__main__": - show_developer_info() diff --git a/.trellis/scripts/common/git.py b/.trellis/scripts/common/git.py deleted file mode 100755 index c4bf29f..0000000 --- a/.trellis/scripts/common/git.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Git command execution utility. - -Single source of truth for running git commands across all Trellis scripts. -""" - -from __future__ import annotations - -import subprocess -from pathlib import Path - - -def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]: - """Run a git command and return (returncode, stdout, stderr). - - Uses UTF-8 encoding with -c i18n.logOutputEncoding=UTF-8 to ensure - consistent output across all platforms (Windows, macOS, Linux). - """ - try: - git_args = ["git", "-c", "i18n.logOutputEncoding=UTF-8"] + args - result = subprocess.run( - git_args, - cwd=cwd, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - return result.returncode, result.stdout, result.stderr - except Exception as e: - return 1, "", str(e) diff --git a/.trellis/scripts/common/git_context.py b/.trellis/scripts/common/git_context.py deleted file mode 100755 index 23fc6ec..0000000 --- a/.trellis/scripts/common/git_context.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Git and Session Context utilities. - -Entry shim — delegates to session_context and packages_context. - -Provides: - output_json - Output context in JSON format - output_text - Output context in text format -""" - -from __future__ import annotations - -import json - -from .git import run_git -from .session_context import ( - get_context_json, - get_context_text, - get_context_record_json, - get_context_text_record, - output_json, - output_text, -) -from .packages_context import ( - get_context_packages_text, - get_context_packages_json, -) -from .trellis_config import read_trellis_config -from .workflow_phase import ( - filter_platform, - get_phase_index, - get_step, - resolve_effective_platform, -) - -# Backward-compatible alias — external modules import this name -_run_git_command = run_git - - -# ============================================================================= -# Main Entry -# ============================================================================= - -def main() -> None: - """CLI entry point.""" - import argparse - - parser = argparse.ArgumentParser(description="Get Session Context for AI Agent") - parser.add_argument( - "--json", - "-j", - action="store_true", - help="Output in JSON format (works with any --mode)", - ) - parser.add_argument( - "--mode", - "-m", - choices=["default", "record", "packages", "phase"], - default="default", - help="Output mode: default (full context), record (for record-session), packages (package info only), phase (workflow step extraction)", - ) - parser.add_argument( - "--step", - help="Step id for --mode phase, e.g. 1.1, 2.2. Omit to get the Phase Index.", - ) - parser.add_argument( - "--platform", - help="Platform name for --mode phase, e.g. cursor, claude-code. Filters platform-tagged blocks.", - ) - - args = parser.parse_args() - - if args.mode == "record": - if args.json: - print(json.dumps(get_context_record_json(), indent=2, ensure_ascii=False)) - else: - print(get_context_text_record()) - elif args.mode == "packages": - if args.json: - print(json.dumps(get_context_packages_json(), indent=2, ensure_ascii=False)) - else: - print(get_context_packages_text()) - elif args.mode == "phase": - content = get_step(args.step) if args.step else get_phase_index() - if not content.strip(): - if args.step: - parser.exit(2, f"Step not found: {args.step}\n") - else: - parser.exit(2, "Phase Index section not found in workflow.md\n") - if args.platform: - effective = resolve_effective_platform( - args.platform, read_trellis_config() - ) - content = filter_platform(content, effective) - print(content, end="") - else: - if args.json: - output_json() - else: - output_text() - - -if __name__ == "__main__": - main() diff --git a/.trellis/scripts/common/io.py b/.trellis/scripts/common/io.py deleted file mode 100755 index 44288f4..0000000 --- a/.trellis/scripts/common/io.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -JSON file I/O utilities. - -Provides read_json and write_json as the single source of truth -for JSON file operations across all Trellis scripts. -""" - -from __future__ import annotations - -import json -from pathlib import Path - - -def read_json(path: Path) -> dict | None: - """Read and parse a JSON file. - - Returns None if the file doesn't exist, is invalid JSON, or can't be read. - """ - try: - return json.loads(path.read_text(encoding="utf-8")) - except (FileNotFoundError, json.JSONDecodeError, OSError): - return None - - -def write_json(path: Path, data: dict) -> bool: - """Write dict to JSON file with pretty formatting. - - Returns True on success, False on error. - """ - try: - path.write_text( - json.dumps(data, indent=2, ensure_ascii=False), - encoding="utf-8", - ) - return True - except (OSError, IOError): - return False diff --git a/.trellis/scripts/common/log.py b/.trellis/scripts/common/log.py deleted file mode 100755 index 839c643..0000000 --- a/.trellis/scripts/common/log.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Terminal output utilities: colors and structured logging. - -Single source of truth for Colors and log_* functions -used across all Trellis scripts. -""" - -from __future__ import annotations - - -class Colors: - """ANSI color codes for terminal output.""" - - RED = "\033[0;31m" - GREEN = "\033[0;32m" - YELLOW = "\033[1;33m" - BLUE = "\033[0;34m" - CYAN = "\033[0;36m" - DIM = "\033[2m" - NC = "\033[0m" # No Color / Reset - - -def colored(text: str, color: str) -> str: - """Apply ANSI color to text.""" - return f"{color}{text}{Colors.NC}" - - -def log_info(msg: str) -> None: - """Print info-level message with [INFO] prefix.""" - print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}") - - -def log_success(msg: str) -> None: - """Print success message with [SUCCESS] prefix.""" - print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}") - - -def log_warn(msg: str) -> None: - """Print warning message with [WARN] prefix.""" - print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}") - - -def log_error(msg: str) -> None: - """Print error message with [ERROR] prefix.""" - print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}") diff --git a/.trellis/scripts/common/packages_context.py b/.trellis/scripts/common/packages_context.py deleted file mode 100755 index e7d4e8c..0000000 --- a/.trellis/scripts/common/packages_context.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python3 -""" -Package discovery and context output. - -Provides: - get_packages_info - Get structured package info - get_packages_section - Build PACKAGES text section - get_context_packages_text - Full packages text output (--mode packages) - get_context_packages_json - Full packages JSON output (--mode packages --json) -""" - -from __future__ import annotations - -from pathlib import Path - -from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope -from .paths import ( - DIR_SPEC, - DIR_WORKFLOW, - get_current_task, - get_repo_root, -) -from .tasks import load_task - - -# ============================================================================= -# Internal Helpers -# ============================================================================= - -def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]: - """Scan spec directory for available layers (subdirectories). - - For monorepo: scans spec/<package>/ - For single-repo: scans spec/ - """ - target = spec_dir / package if package else spec_dir - if not target.is_dir(): - return [] - return sorted( - d.name for d in target.iterdir() if d.is_dir() and d.name != "guides" - ) - - -def _get_active_task_package(repo_root: Path) -> str | None: - """Get the package field from the active task's task.json.""" - current = get_current_task(repo_root) - if not current: - return None - ct = load_task(repo_root / current) - return ct.package if ct and ct.package else None - - -def _resolve_scope_set( - packages: dict, - spec_scope, - task_pkg: str | None, - default_pkg: str | None, -) -> set | None: - """Resolve spec_scope to a set of allowed package names, or None for full scan.""" - if not packages: - return None - - if spec_scope is None: - return None - - if isinstance(spec_scope, str) and spec_scope == "active_task": - if task_pkg and task_pkg in packages: - return {task_pkg} - if default_pkg and default_pkg in packages: - return {default_pkg} - return None - - if isinstance(spec_scope, list): - valid = {e for e in spec_scope if e in packages} - if valid: - return valid - # All invalid: fallback - if task_pkg and task_pkg in packages: - return {task_pkg} - if default_pkg and default_pkg in packages: - return {default_pkg} - return None - - return None - - -# ============================================================================= -# Public Functions -# ============================================================================= - -def get_packages_info(repo_root: Path) -> list[dict]: - """Get structured package info for monorepo projects. - - Returns list of dicts with keys: name, path, type, default, specLayers, - isSubmodule, isGitRepo. - Returns empty list for single-repo projects. - """ - packages = get_packages(repo_root) - if not packages: - return [] - - default_pkg = get_default_package(repo_root) - spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC - result = [] - - for pkg_name, pkg_config in packages.items(): - pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config) - pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local" - pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False - layers = _scan_spec_layers(spec_dir, pkg_name) - - result.append({ - "name": pkg_name, - "path": pkg_path, - "type": pkg_type, - "default": pkg_name == default_pkg, - "specLayers": layers, - "isSubmodule": pkg_type == "submodule", - "isGitRepo": _is_true_config_value(pkg_git), - }) - - return result - - -def get_packages_section(repo_root: Path) -> str: - """Build the PACKAGES section for text output.""" - spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC - pkg_info = get_packages_info(repo_root) - - lines: list[str] = [] - lines.append("## PACKAGES") - - if not pkg_info: - lines.append("(single-repo mode)") - layers = _scan_spec_layers(spec_dir) - if layers: - lines.append(f"Spec layers: {', '.join(layers)}") - return "\n".join(lines) - - default_pkg = get_default_package(repo_root) - - for pkg in pkg_info: - layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else "" - submodule_tag = " (submodule)" if pkg["isSubmodule"] else "" - git_repo_tag = " (git repo)" if pkg["isGitRepo"] else "" - default_tag = " *" if pkg["default"] else "" - lines.append( - f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}" - ) - - if default_pkg: - lines.append(f"Default package: {default_pkg}") - - return "\n".join(lines) - - -def get_context_packages_text(repo_root: Path | None = None) -> str: - """Get packages context as formatted text (for --mode packages).""" - if repo_root is None: - repo_root = get_repo_root() - - pkg_info = get_packages_info(repo_root) - lines: list[str] = [] - - if not pkg_info: - spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC - lines.append("Single-repo project (no packages configured)") - lines.append("") - layers = _scan_spec_layers(spec_dir) - if layers: - lines.append(f"Spec layers: {', '.join(layers)}") - return "\n".join(lines) - - # Resolve scope for annotations - packages_dict = get_packages(repo_root) or {} - default_pkg = get_default_package(repo_root) - spec_scope = get_spec_scope(repo_root) - task_pkg = _get_active_task_package(repo_root) - scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg) - - lines.append("## PACKAGES") - lines.append("") - for pkg in pkg_info: - default_tag = " (default)" if pkg["default"] else "" - type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else "" - git_tag = " [git repo]" if pkg["isGitRepo"] else "" - - # Scope annotation - scope_tag = "" - if scope_set is not None and pkg["name"] not in scope_set: - scope_tag = " (out of scope)" - - lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}") - lines.append(f"Path: {pkg['path']}") - if pkg["specLayers"]: - lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}") - for layer in pkg["specLayers"]: - lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md") - else: - lines.append("Spec: not configured") - lines.append("") - - # Also show shared guides - guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides" - if guides_dir.is_dir(): - lines.append("### Shared Guides (always included)") - lines.append("Path: .trellis/spec/guides/index.md") - lines.append("") - - return "\n".join(lines) - - -def get_context_packages_json(repo_root: Path | None = None) -> dict: - """Get packages context as a dictionary (for --mode packages --json).""" - if repo_root is None: - repo_root = get_repo_root() - - pkg_info = get_packages_info(repo_root) - - if not pkg_info: - spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC - layers = _scan_spec_layers(spec_dir) - return { - "mode": "single-repo", - "specLayers": layers, - } - - default_pkg = get_default_package(repo_root) - spec_scope = get_spec_scope(repo_root) - task_pkg = _get_active_task_package(repo_root) - - return { - "mode": "monorepo", - "packages": pkg_info, - "defaultPackage": default_pkg, - "specScope": spec_scope, - "activeTaskPackage": task_pkg, - } diff --git a/.trellis/scripts/common/paths.py b/.trellis/scripts/common/paths.py deleted file mode 100755 index 1c5a58e..0000000 --- a/.trellis/scripts/common/paths.py +++ /dev/null @@ -1,447 +0,0 @@ -#!/usr/bin/env python3 -""" -Common path utilities for Trellis workflow. - -Provides: - get_repo_root - Get repository root directory - get_developer - Get developer name - get_workspace_dir - Get developer workspace directory - get_tasks_dir - Get tasks directory - get_active_journal_file - Get current journal file -""" - -from __future__ import annotations - -import re -from datetime import datetime -from pathlib import Path - - -# ============================================================================= -# Path Constants (change here to rename directories) -# ============================================================================= - -# Directory names -DIR_WORKFLOW = ".trellis" -DIR_WORKSPACE = "workspace" -DIR_TASKS = "tasks" -DIR_ARCHIVE = "archive" -DIR_SPEC = "spec" -DIR_SCRIPTS = "scripts" - -# File names -FILE_DEVELOPER = ".developer" -FILE_CURRENT_TASK = ".current-task" -FILE_TASK_JSON = "task.json" -FILE_JOURNAL_PREFIX = "journal-" - - -# ============================================================================= -# Repository Root -# ============================================================================= - -def get_repo_root(start_path: Path | None = None) -> Path: - """Find the nearest directory containing .trellis/ folder. - - This handles nested git repos correctly (e.g., test project inside another repo). - - Args: - start_path: Starting directory to search from. Defaults to current directory. - - Returns: - Path to repository root, or current directory if no .trellis/ found. - """ - current = (start_path or Path.cwd()).resolve() - - while current != current.parent: - if (current / DIR_WORKFLOW).is_dir(): - return current - current = current.parent - - # Fallback to current directory if no .trellis/ found - return Path.cwd().resolve() - - -# ============================================================================= -# Developer -# ============================================================================= - -def get_developer(repo_root: Path | None = None) -> str | None: - """Get developer name from .developer file. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Developer name or None if not initialized. - """ - if repo_root is None: - repo_root = get_repo_root() - - dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER - - if not dev_file.is_file(): - return None - - try: - content = dev_file.read_text(encoding="utf-8") - for line in content.splitlines(): - if line.startswith("name="): - return line.split("=", 1)[1].strip() - except (OSError, IOError): - pass - - return None - - -def check_developer(repo_root: Path | None = None) -> bool: - """Check if developer is initialized. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True if developer is initialized. - """ - return get_developer(repo_root) is not None - - -# ============================================================================= -# Tasks Directory -# ============================================================================= - -def get_tasks_dir(repo_root: Path | None = None) -> Path: - """Get tasks directory path. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Path to tasks directory. - """ - if repo_root is None: - repo_root = get_repo_root() - return repo_root / DIR_WORKFLOW / DIR_TASKS - - -# ============================================================================= -# Workspace Directory -# ============================================================================= - -def get_workspace_dir(repo_root: Path | None = None) -> Path | None: - """Get developer workspace directory. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Path to workspace directory or None if developer not set. - """ - if repo_root is None: - repo_root = get_repo_root() - - developer = get_developer(repo_root) - if developer: - return repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer - return None - - -# ============================================================================= -# Journal File -# ============================================================================= - -def get_active_journal_file(repo_root: Path | None = None) -> Path | None: - """Get the current active journal file. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Path to active journal file or None if not found. - """ - if repo_root is None: - repo_root = get_repo_root() - - workspace_dir = get_workspace_dir(repo_root) - if workspace_dir is None or not workspace_dir.is_dir(): - return None - - latest: Path | None = None - highest = 0 - - for f in workspace_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"): - if not f.is_file(): - continue - - # Extract number from filename - name = f.stem # e.g., "journal-1" - match = re.search(r"(\d+)$", name) - if match: - num = int(match.group(1)) - if num > highest: - highest = num - latest = f - - return latest - - -def count_lines(file_path: Path) -> int: - """Count lines in a file. - - Args: - file_path: Path to file. - - Returns: - Number of lines, or 0 if file doesn't exist. - """ - if not file_path.is_file(): - return 0 - - try: - return len(file_path.read_text(encoding="utf-8").splitlines()) - except (OSError, IOError): - return 0 - - -# ============================================================================= -# Current Task Management -# ============================================================================= - -def normalize_task_ref(task_ref: str) -> str: - """Normalize a task ref for stable runtime storage. - - Stored refs should prefer repo-relative POSIX paths like - `.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved - unless they can later be converted back to repo-relative form by callers. - """ - normalized = task_ref.strip() - if not normalized: - return "" - - path_obj = Path(normalized) - if path_obj.is_absolute(): - return str(path_obj) - - normalized = normalized.replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - - if normalized.startswith(f"{DIR_TASKS}/"): - return f"{DIR_WORKFLOW}/{normalized}" - - return normalized - - -def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None: - """Resolve a task ref to an absolute task directory path.""" - if repo_root is None: - repo_root = get_repo_root() - - normalized = normalize_task_ref(task_ref) - if not normalized: - return None - - path_obj = Path(normalized) - if path_obj.is_absolute(): - return path_obj - - if normalized.startswith(f"{DIR_WORKFLOW}/"): - return repo_root / path_obj - - return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj - - -def get_current_task( - repo_root: Path | None = None, - platform_input: dict | None = None, - platform: str | None = None, -) -> str | None: - """Get current task directory path (relative to repo_root). - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Relative path to current task directory or None. - """ - if repo_root is None: - repo_root = get_repo_root() - - from .active_task import resolve_active_task - - return resolve_active_task(repo_root, platform_input, platform).task_path - - -def get_current_task_abs( - repo_root: Path | None = None, - platform_input: dict | None = None, - platform: str | None = None, -) -> Path | None: - """Get current task directory absolute path. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Absolute path to current task directory or None. - """ - if repo_root is None: - repo_root = get_repo_root() - - relative = get_current_task(repo_root, platform_input, platform) - if relative: - return resolve_task_ref(relative, repo_root) - return None - - -def get_current_task_source( - repo_root: Path | None = None, - platform_input: dict | None = None, - platform: str | None = None, -) -> tuple[str, str | None, str | None]: - """Get active task source as (`source`, `context_key`, `task_path`).""" - if repo_root is None: - repo_root = get_repo_root() - - from .active_task import get_current_task_source as _get_source - - return _get_source(repo_root, platform_input, platform) - - -def set_current_task( - task_path: str, - repo_root: Path | None = None, - platform_input: dict | None = None, - platform: str | None = None, -) -> bool: - """Set current task in session scope. - - Args: - task_path: Task directory path (relative to repo_root). - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True on success, False on error. - """ - if repo_root is None: - repo_root = get_repo_root() - - from .active_task import set_active_task - - return set_active_task( - task_path, - repo_root, - platform_input=platform_input, - platform=platform, - ) is not None - - -def clear_current_task( - repo_root: Path | None = None, - platform_input: dict | None = None, - platform: str | None = None, -) -> bool: - """Clear current task in session scope. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True on success. - """ - if repo_root is None: - repo_root = get_repo_root() - - from .active_task import clear_active_task - - clear_active_task( - repo_root, - platform_input=platform_input, - platform=platform, - ) - return True - - -def has_current_task(repo_root: Path | None = None) -> bool: - """Check if has current task. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True if current task is set. - """ - return get_current_task(repo_root) is not None - - -# ============================================================================= -# Task ID Generation -# ============================================================================= - -def generate_task_date_prefix() -> str: - """Generate task ID based on date (MM-DD format). - - Returns: - Date prefix string (e.g., "01-21"). - """ - return datetime.now().strftime("%m-%d") - - -# ============================================================================= -# Monorepo / Package Paths -# ============================================================================= - - -def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path: - """Get the spec directory path. - - Single-repo: .trellis/spec - Monorepo with package: .trellis/spec/<package> - - Uses lazy import to avoid circular dependency with config.py. - """ - if repo_root is None: - repo_root = get_repo_root() - - from .config import get_spec_base - - base = get_spec_base(package, repo_root) - return repo_root / DIR_WORKFLOW / base - - -def get_package_path(package: str, repo_root: Path | None = None) -> Path | None: - """Get a package's source directory absolute path from config. - - Returns: - Absolute path to the package directory, or None if not found. - """ - if repo_root is None: - repo_root = get_repo_root() - - from .config import get_packages - - packages = get_packages(repo_root) - if not packages or package not in packages: - return None - - info = packages[package] - if isinstance(info, dict): - rel_path = info.get("path", package) - else: - rel_path = str(info) - - return repo_root / rel_path - - -# ============================================================================= -# Main Entry (for testing) -# ============================================================================= - -if __name__ == "__main__": - repo = get_repo_root() - print(f"Repository root: {repo}") - print(f"Developer: {get_developer(repo)}") - print(f"Tasks dir: {get_tasks_dir(repo)}") - print(f"Workspace dir: {get_workspace_dir(repo)}") - print(f"Journal file: {get_active_journal_file(repo)}") - print(f"Current task: {get_current_task(repo)}") diff --git a/.trellis/scripts/common/safe_commit.py b/.trellis/scripts/common/safe_commit.py deleted file mode 100755 index 4174191..0000000 --- a/.trellis/scripts/common/safe_commit.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -Safe git-add helpers for Trellis-owned paths. - -Why this module exists ----------------------- -A real user incident: a project's `.gitignore` listed `.trellis/` (company-wide -template / personal habit). When `add_session.py` and `task.py archive` ran -their auto-commit and `git add` failed with `ignored by .gitignore`, the AI -agent driving the workflow "fixed" it by retrying with -`git add -f .trellis/` — which fan-out-included every ignored subtree -(`.trellis/.backup-*/`, `.trellis/worktrees/`, `.trellis/.template-hashes.json`, -`.trellis/.runtime/`), committing 548 files / 83474 lines of caches/backups. - -Design ------- -- Scripts only stage SPECIFIC product paths (journal files, index.md, the - current task dir, the archive dir). Never the whole `.trellis/` tree. -- If plain `git add <specific>` fails with "ignored by", DO NOT retry with - ``-f``. The presence of `.trellis/` in `.gitignore` is treated as user - intent ("keep .trellis/ local-only"). The script warns and skips the - auto-commit; users who want auto-staging can either fix their `.gitignore` - or set ``session_auto_commit: false`` and manage git themselves. -- The warning includes a negative example: ``Do NOT use `git add -f .trellis/` ...`` - so any AI rereading the log doesn't reinvent the bug. - -History note: 0.5.10 introduced an automatic ``git add -f`` retry on the -specific paths. That was reverted in 0.5.11 — auto-forcing into a tree the -user had gitignored violates user intent even when the path list is narrow. -The wider-grain forbidden command stays forbidden, and the narrow-grain auto -``-f`` is gone too. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -from .git import run_git -from .paths import ( - DIR_ARCHIVE, - DIR_TASKS, - DIR_WORKFLOW, - DIR_WORKSPACE, - FILE_JOURNAL_PREFIX, - get_developer, -) - - -# Paths under .trellis/ that must NEVER be auto-staged. Listed here so the -# warning to the user can show concrete subpaths to ignore individually -# instead of ignoring the whole `.trellis/` tree. -TRELLIS_IGNORED_SUBPATHS = ( - ".trellis/.backup-*", - ".trellis/worktrees/", - ".trellis/.template-hashes.json", - ".trellis/.runtime/", - ".trellis/.cache/", -) - - -def safe_trellis_paths_to_add(repo_root: Path) -> list[str]: - """Return the list of repo-relative paths the auto-commit should stage. - - Only includes paths that exist on disk so callers don't pass non-existent - arguments to git. The caller is responsible for `git diff --cached` - checking afterwards. - - Included: - - .trellis/workspace/<developer>/journal-*.md - - .trellis/workspace/<developer>/index.md - - .trellis/tasks/<task-dir>/ (every active task directory) - - .trellis/tasks/archive/ (whole archive subtree, if present) - - Excluded (intentionally — these must not be staged): - - .trellis/.backup-*, .trellis/worktrees/, - .trellis/.template-hashes.json, .trellis/.runtime/, .trellis/.cache/ - """ - paths: list[str] = [] - - # Workspace journal files + index.md - developer = get_developer(repo_root) - if developer: - ws = repo_root / DIR_WORKFLOW / DIR_WORKSPACE / developer - if ws.is_dir(): - for f in sorted(ws.glob(f"{FILE_JOURNAL_PREFIX}*.md")): - if f.is_file(): - paths.append( - f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{f.name}" - ) - index_md = ws / "index.md" - if index_md.is_file(): - paths.append( - f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/index.md" - ) - - # Active tasks: each direct child of tasks/ that is a directory and not - # the archive root. The archive subtree is added as a single path below. - tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS - if tasks_dir.is_dir(): - for child in sorted(tasks_dir.iterdir()): - if not child.is_dir(): - continue - if child.name == DIR_ARCHIVE: - continue - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}") - - archive_dir = tasks_dir / DIR_ARCHIVE - if archive_dir.is_dir(): - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}") - - return paths - - -def safe_archive_paths_to_add( - repo_root: Path, - task_name: str | None = None, - modified_children: list[str] | None = None, -) -> list[str]: - """Return paths to stage after `task.py archive`. - - Scoped to ONLY the paths the archive operation actually touched: - - - the archive subtree (where the freshly-moved task lives) - - the source task directory (for source-side deletes; caller pairs - this with `git rm --cached` since `git add` won't stage deletes - for a path that no longer exists in the working tree) - - any child task directories whose `task.json` was edited to drop - the archived parent (parent-children relationship update) - - This narrow scope avoids "scope creep" — dirty changes in OTHER - active task dirs (parallel-window edits) are NOT bundled into the - archive commit. Callers handle each kind of change in its own - commit boundary. - - Backwards-compat: with no arguments, the function walks the whole - `.trellis/tasks/` subtree the old way (active tasks + archive). New - callers should always pass `task_name`. - """ - paths: list[str] = [] - tasks_dir = repo_root / DIR_WORKFLOW / DIR_TASKS - if not tasks_dir.is_dir(): - return paths - - archive_dir = tasks_dir / DIR_ARCHIVE - - if task_name is not None: - # Narrow scope — only paths that still exist on disk (so - # `git add` doesn't choke on the moved-away source). The caller - # handles the source-side deletes via `git rm --cached` - # explicitly. - if archive_dir.is_dir(): - paths.append( - f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}" - ) - for child_name in modified_children or []: - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child_name}") - return paths - - # Legacy wide scope (no task_name): preserve old behavior so callers - # that have not been updated keep working. - if archive_dir.is_dir(): - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}") - for child in sorted(tasks_dir.iterdir()): - if not child.is_dir(): - continue - if child.name == DIR_ARCHIVE: - continue - paths.append(f"{DIR_WORKFLOW}/{DIR_TASKS}/{child.name}") - return paths - - -def _stderr_indicates_ignored(stderr: str) -> bool: - """git add error indicates the path is excluded by .gitignore.""" - if not stderr: - return False - lowered = stderr.lower() - return "ignored by" in lowered - - -def safe_git_add( - paths: list[str], repo_root: Path -) -> tuple[bool, bool, str]: - """Run `git add` on specific paths; never retry with -f. - - Returns ``(success, used_force, stderr)``. The ``used_force`` field is - kept for signature compatibility with the 0.5.10 implementation but is - always ``False`` — we never auto-force. - - Behavior: - - No paths passed → success, no force, empty stderr. - - Plain ``git add -- <paths>`` succeeds → return success. - - Plain fails (any reason — ignored or otherwise) → return failure with - the stderr. Callers should inspect the stderr (see - :func:`print_gitignore_warning`) and skip the auto-commit. - """ - if not paths: - return True, False, "" - - rc, _, err = run_git(["add", "--", *paths], cwd=repo_root) - if rc == 0: - return True, False, "" - return False, False, err - - -def print_gitignore_warning(paths: list[str]) -> None: - """Explain to the user (and any AI reading the log) what to do. - - CRITICAL: includes the negative example - ``Do NOT use `git add -f .trellis/``` — agents reading the warning are - known to invent that command, which fans out to ignored caches/backups. - """ - print( - "[WARN] git add failed because .trellis/ paths are ignored by your .gitignore.", - file=sys.stderr, - ) - print( - "[WARN] Skipping auto-commit. The journal/task files were still written to disk;", - file=sys.stderr, - ) - print( - "[WARN] git was not touched.", - file=sys.stderr, - ) - print("[WARN]", file=sys.stderr) - print( - "[WARN] Trellis manages these specific paths and they should be tracked:", - file=sys.stderr, - ) - if paths: - for p in paths: - print(f"[WARN] {p}", file=sys.stderr) - else: - print( - "[WARN] .trellis/workspace/<developer>/{journal-*.md,index.md}", - file=sys.stderr, - ) - print( - "[WARN] .trellis/tasks/<task-dir>/", - file=sys.stderr, - ) - print( - "[WARN] .trellis/tasks/archive/", - file=sys.stderr, - ) - print("[WARN]", file=sys.stderr) - print( - "[WARN] Recommended: change your .gitignore from `.trellis/` to specific", - file=sys.stderr, - ) - print( - "[WARN] subpaths that should remain ignored, e.g.:", - file=sys.stderr, - ) - for sub in TRELLIS_IGNORED_SUBPATHS: - print(f"[WARN] {sub}", file=sys.stderr) - print("[WARN]", file=sys.stderr) - print( - "[WARN] Or, if you intentionally keep .trellis/ local-only, set in", - file=sys.stderr, - ) - print( - "[WARN] .trellis/config.yaml:", - file=sys.stderr, - ) - print( - "[WARN] session_auto_commit: false", - file=sys.stderr, - ) - print( - "[WARN] so the scripts skip git entirely and you can review / commit", - file=sys.stderr, - ) - print( - "[WARN] manually with `git status` / `git add` / `git commit`.", - file=sys.stderr, - ) - print("[WARN]", file=sys.stderr) - print( - "[WARN] Do NOT use `git add -f .trellis/` — it pulls in backups, worktrees,", - file=sys.stderr, - ) - print( - "[WARN] and runtime caches that should never be committed.", - file=sys.stderr, - ) diff --git a/.trellis/scripts/common/session_context.py b/.trellis/scripts/common/session_context.py deleted file mode 100755 index 772c7aa..0000000 --- a/.trellis/scripts/common/session_context.py +++ /dev/null @@ -1,821 +0,0 @@ -#!/usr/bin/env python3 -""" -Session context generation (default + record modes). - -Provides: - get_context_json - JSON output for default mode - get_context_text - Text output for default mode - get_context_record_json - JSON for record mode - get_context_text_record - Text for record mode - output_json - Print JSON - output_text - Print text -""" - -from __future__ import annotations - -import json -import os -import re -import subprocess -from pathlib import Path - -from .active_task import resolve_context_key -from .config import get_git_packages -from .git import run_git -from .packages_context import get_packages_section -from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress -from .paths import ( - DIR_SCRIPTS, - DIR_SPEC, - DIR_TASKS, - DIR_WORKFLOW, - DIR_WORKSPACE, - count_lines, - get_active_journal_file, - get_current_task, - get_current_task_source, - get_developer, - get_repo_root, - get_tasks_dir, -) - - -# ============================================================================= -# Helpers -# ============================================================================= - -_PACKAGE_NAME = "@mindfoldhq/trellis" -_UPDATE_CHECK_TIMEOUT_SECONDS = 1.0 -_VERSION_RE = re.compile( - r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$" -) -_VERSION_TOKEN_RE = re.compile(r"\b\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?\b") -_POLYREPO_IGNORED_DIRS = { - "node_modules", - "target", - "dist", - "build", - "out", - "bin", - "obj", - "vendor", - "coverage", - "tmp", - "__pycache__", -} -_POLYREPO_SCAN_MAX_DEPTH = 2 - - -def _is_git_worktree(path: Path) -> bool: - """Return True when path is inside a Git worktree.""" - rc, out, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=path) - return rc == 0 and out.strip().lower() == "true" - - -def _parse_recent_commits(log_output: str) -> list[dict]: - """Parse `git log --oneline` output into structured commit entries.""" - commits = [] - for line in log_output.splitlines(): - if not line.strip(): - continue - parts = line.split(" ", 1) - if len(parts) >= 2: - commits.append({"hash": parts[0], "message": parts[1]}) - elif len(parts) == 1: - commits.append({"hash": parts[0], "message": ""}) - return commits - - -def _collect_git_repo_info(name: str, rel_path: str, repo_dir: Path) -> dict | None: - """Collect Git status for one known repository directory.""" - if not (repo_dir / ".git").exists(): - return None - - _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_dir) - branch = branch_out.strip() or "unknown" - - _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_dir) - changes = len([l for l in status_out.splitlines() if l.strip()]) - - _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_dir) - - return { - "name": name, - "path": rel_path, - "branch": branch, - "isClean": changes == 0, - "uncommittedChanges": changes, - "recentCommits": _parse_recent_commits(log_out), - } - - -def _collect_root_git_info(repo_root: Path) -> dict: - """Collect root Git info without pretending a non-Git root is clean.""" - if not _is_git_worktree(repo_root): - return { - "isRepo": False, - "branch": "", - "isClean": False, - "uncommittedChanges": 0, - "recentCommits": [], - } - - _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) - branch = branch_out.strip() or "unknown" - - _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root) - status_lines = [line for line in status_out.splitlines() if line.strip()] - - _, short_out, _ = run_git(["status", "--short"], cwd=repo_root) - - _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root) - - return { - "isRepo": True, - "branch": branch, - "isClean": len(status_lines) == 0, - "uncommittedChanges": len(status_lines), - "statusShort": short_out.splitlines(), - "recentCommits": _parse_recent_commits(log_out), - } - - -def _discover_child_git_repos(repo_root: Path) -> list[tuple[str, str]]: - """Discover child Git repositories using the init-time polyrepo heuristic.""" - found: list[str] = [] - - def is_candidate_dir(path: Path) -> bool: - name = path.name - return not name.startswith(".") and name not in _POLYREPO_IGNORED_DIRS - - def scan(rel_dir: Path, depth: int) -> None: - if depth >= _POLYREPO_SCAN_MAX_DEPTH: - return - abs_dir = repo_root / rel_dir - try: - children = sorted(abs_dir.iterdir(), key=lambda p: p.name) - except OSError: - return - - for child in children: - if not child.is_dir() or not is_candidate_dir(child): - continue - - child_rel = ( - rel_dir / child.name if rel_dir != Path(".") else Path(child.name) - ) - if (child / ".git").exists(): - found.append(child_rel.as_posix()) - continue - scan(child_rel, depth + 1) - - scan(Path("."), 0) - if len(found) < 2: - return [] - return [(path.replace("/", "_"), path) for path in sorted(found)] - - -def _collect_package_git_info( - repo_root: Path, - discover_unconfigured: bool = False, -) -> list[dict]: - """Collect Git status for independent package repositories. - - Packages marked with ``git: true`` in config.yaml are authoritative. - When the Trellis root is not a Git repo and no configured package repos are - available, optionally fall back to the bounded polyrepo child scan. - - Returns: - List of dicts with keys: name, path, branch, isClean, - uncommittedChanges, recentCommits. - Empty list if no git-repo packages are configured. - """ - git_pkgs = get_git_packages(repo_root) - result = [] - for pkg_name, pkg_path in git_pkgs.items(): - pkg_dir = repo_root / pkg_path - info = _collect_git_repo_info(pkg_name, pkg_path, pkg_dir) - if info is not None: - result.append(info) - - if result or not discover_unconfigured: - return result - - discovered = [] - for pkg_name, pkg_path in _discover_child_git_repos(repo_root): - info = _collect_git_repo_info(pkg_name, pkg_path, repo_root / pkg_path) - if info is not None: - discovered.append(info) - return discovered - - -def _append_root_git_context(lines: list[str], root_git_info: dict) -> None: - """Append root Git status without misleading non-Git roots.""" - lines.append("## GIT STATUS") - if not root_git_info["isRepo"]: - lines.append("Root is not a Git repository.") - lines.append("Run Git commands from the package repository paths listed below.") - else: - lines.append(f"Branch: {root_git_info['branch']}") - if root_git_info["isClean"]: - lines.append("Working directory: Clean") - else: - lines.append( - f"Working directory: {root_git_info['uncommittedChanges']} " - "uncommitted change(s)" - ) - lines.append("") - lines.append("Changes:") - for line in root_git_info.get("statusShort", [])[:10]: - lines.append(line) - lines.append("") - - lines.append("## RECENT COMMITS") - if not root_git_info["isRepo"]: - lines.append( - "Root has no Git commit history because it is not a Git repository." - ) - elif root_git_info["recentCommits"]: - for commit in root_git_info["recentCommits"]: - lines.append(f"{commit['hash']} {commit['message']}") - else: - lines.append("(no commits)") - lines.append("") - - -def _append_package_git_context(lines: list[str], package_git_info: list[dict]) -> None: - """Append Git status and recent commits for package repositories.""" - for pkg in package_git_info: - lines.append(f"## GIT STATUS ({pkg['name']}: {pkg['path']})") - lines.append(f"Branch: {pkg['branch']}") - if pkg["isClean"]: - lines.append("Working directory: Clean") - else: - lines.append( - f"Working directory: {pkg['uncommittedChanges']} uncommitted change(s)" - ) - lines.append("") - lines.append(f"## RECENT COMMITS ({pkg['name']}: {pkg['path']})") - if pkg["recentCommits"]: - for commit in pkg["recentCommits"]: - lines.append(f"{commit['hash']} {commit['message']}") - else: - lines.append("(no commits)") - lines.append("") - - -def _read_project_version(repo_root: Path) -> str | None: - try: - version = (repo_root / DIR_WORKFLOW / ".version").read_text( - encoding="utf-8" - ).strip() - except OSError: - return None - return version or None - - -def _fetch_trellis_version_output() -> str | None: - try: - result = subprocess.run( - ["trellis", "--version"], - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - timeout=_UPDATE_CHECK_TIMEOUT_SECONDS, - ) - except (OSError, subprocess.SubprocessError, TimeoutError): - return None - - if result.returncode != 0: - return None - output = f"{result.stdout}\n{result.stderr}".strip() - return output or None - - -def _extract_available_update_version(output: str) -> str | None: - update_match = re.search( - r"Trellis update available:\s*" - r"(?P<current>\S+)\s*(?:→|->)\s*(?P<latest>\S+)", - output, - ) - if update_match: - return update_match.group("latest").strip() - candidates = _VERSION_TOKEN_RE.findall(output) - return candidates[-1] if candidates else None - - -def _resolve_available_update_version() -> str | None: - output = _fetch_trellis_version_output() - if not output: - return None - return _extract_available_update_version(output) - - -def _parse_version(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None] | None: - match = _VERSION_RE.match(version) - if not match: - return None - major, minor, patch, prerelease = match.groups() - numbers = (int(major), int(minor or "0"), int(patch or "0")) - prerelease_parts = tuple(prerelease.split(".")) if prerelease else None - return numbers, prerelease_parts - - -def _compare_prerelease( - left: tuple[str, ...] | None, - right: tuple[str, ...] | None, -) -> int: - if left is None and right is None: - return 0 - if left is None: - return 1 - if right is None: - return -1 - - for left_part, right_part in zip(left, right): - if left_part == right_part: - continue - left_numeric = left_part.isdigit() - right_numeric = right_part.isdigit() - if left_numeric and right_numeric: - left_int = int(left_part) - right_int = int(right_part) - return (left_int > right_int) - (left_int < right_int) - if left_numeric: - return -1 - if right_numeric: - return 1 - return (left_part > right_part) - (left_part < right_part) - - return (len(left) > len(right)) - (len(left) < len(right)) - - -def _compare_versions(left: str, right: str) -> int | None: - parsed_left = _parse_version(left) - parsed_right = _parse_version(right) - if parsed_left is None or parsed_right is None: - return None - - left_numbers, left_prerelease = parsed_left - right_numbers, right_prerelease = parsed_right - if left_numbers != right_numbers: - return (left_numbers > right_numbers) - (left_numbers < right_numbers) - return _compare_prerelease(left_prerelease, right_prerelease) - - -def _update_marker_path(repo_root: Path) -> Path: - context_key = resolve_context_key() - if not context_key: - terminal_key = os.environ.get("TERM_SESSION_ID", "").strip() - context_key = terminal_key or f"ppid-{os.getppid()}" - safe_key = re.sub(r"[^A-Za-z0-9._-]+", "_", context_key).strip("._-") - if not safe_key: - safe_key = "session" - return ( - repo_root - / DIR_WORKFLOW - / ".runtime" - / f"update-check-{safe_key[:160]}.marker" - ) - - -def _mark_update_check_attempted(repo_root: Path) -> bool: - marker_path = _update_marker_path(repo_root) - if marker_path.exists(): - return False - try: - marker_path.parent.mkdir(parents=True, exist_ok=True) - marker_path.write_text("checked\n", encoding="utf-8") - except OSError: - pass - return True - - -def _get_update_hint(repo_root: Path) -> str | None: - marker_path = _update_marker_path(repo_root) - if marker_path.exists(): - return None - - current_version = _read_project_version(repo_root) - if not current_version: - return None - - latest_version = _resolve_available_update_version() - if not latest_version: - return None - - _mark_update_check_attempted(repo_root) - comparison = _compare_versions(current_version, latest_version) - if comparison is None or comparison >= 0: - return None - - return ( - f"Trellis update available: {current_version} -> {latest_version}, " - f"run npm install -g {_PACKAGE_NAME}@latest" - ) - - -# ============================================================================= -# JSON Output -# ============================================================================= - -def get_context_json(repo_root: Path | None = None) -> dict: - """Get context as a dictionary. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Context dictionary. - """ - if repo_root is None: - repo_root = get_repo_root() - - developer = get_developer(repo_root) - tasks_dir = get_tasks_dir(repo_root) - journal_file = get_active_journal_file(repo_root) - - journal_lines = 0 - journal_relative = "" - if journal_file and developer: - journal_lines = count_lines(journal_file) - journal_relative = ( - f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}" - ) - - root_git_info = _collect_root_git_info(repo_root) - - # Tasks - tasks = [ - { - "dir": t.dir_name, - "name": t.name, - "status": t.status, - "children": list(t.children), - "parent": t.parent, - } - for t in iter_active_tasks(tasks_dir) - ] - - # Package git repos (independent sub-repositories) - pkg_git_info = _collect_package_git_info( - repo_root, - discover_unconfigured=not root_git_info["isRepo"], - ) - - result = { - "developer": developer or "", - "git": { - "isRepo": root_git_info["isRepo"], - "branch": root_git_info["branch"], - "isClean": root_git_info["isClean"], - "uncommittedChanges": root_git_info["uncommittedChanges"], - "recentCommits": root_git_info["recentCommits"], - }, - "tasks": { - "active": tasks, - "directory": f"{DIR_WORKFLOW}/{DIR_TASKS}", - }, - "journal": { - "file": journal_relative, - "lines": journal_lines, - "nearLimit": journal_lines > 1800, - }, - } - - if pkg_git_info: - result["packageGit"] = pkg_git_info - - return result - - -def output_json(repo_root: Path | None = None) -> None: - """Output context in JSON format. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - """ - context = get_context_json(repo_root) - print(json.dumps(context, indent=2, ensure_ascii=False)) - - -# ============================================================================= -# Text Output -# ============================================================================= - -def get_context_text(repo_root: Path | None = None) -> str: - """Get context as formatted text. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Formatted text output. - """ - if repo_root is None: - repo_root = get_repo_root() - - lines = [] - lines.append("========================================") - lines.append("SESSION CONTEXT") - lines.append("========================================") - lines.append("") - - developer = get_developer(repo_root) - - # Developer section - lines.append("## DEVELOPER") - if not developer: - lines.append( - f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>" - ) - return "\n".join(lines) - - lines.append(f"Name: {developer}") - lines.append("") - - root_git_info = _collect_root_git_info(repo_root) - _append_root_git_context(lines, root_git_info) - - # Package git repos — independent sub-repositories - _append_package_git_context( - lines, - _collect_package_git_info( - repo_root, - discover_unconfigured=not root_git_info["isRepo"], - ), - ) - - # Current task - lines.append("## CURRENT TASK") - current_task = get_current_task(repo_root) - if current_task: - current_task_dir = repo_root / current_task - source_type, context_key, _ = get_current_task_source(repo_root) - lines.append(f"Path: {current_task}") - lines.append( - f"Source: {source_type}" + (f":{context_key}" if context_key else "") - ) - - ct = load_task(current_task_dir) - if ct: - lines.append(f"Name: {ct.name}") - lines.append(f"Status: {ct.status}") - lines.append(f"Created: {ct.raw.get('createdAt', 'unknown')}") - if ct.description: - lines.append(f"Description: {ct.description}") - - # Check for prd.md - prd_file = current_task_dir / "prd.md" - if prd_file.is_file(): - lines.append("") - lines.append("[!] This task has prd.md - read it for task details") - else: - lines.append("(none)") - lines.append("") - - # Active tasks - lines.append("## ACTIVE TASKS") - tasks_dir = get_tasks_dir(repo_root) - task_count = 0 - - # Collect all task data for hierarchy display - all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)} - all_statuses = {name: t.status for name, t in all_tasks.items()} - - def _print_task_tree(name: str, indent: int = 0) -> None: - nonlocal task_count - t = all_tasks[name] - progress = children_progress(t.children, all_statuses) - prefix = " " * indent - lines.append(f"{prefix}- {name}/ ({t.status}){progress} @{t.assignee or '-'}") - task_count += 1 - for child in t.children: - if child in all_tasks: - _print_task_tree(child, indent + 1) - - for dir_name in sorted(all_tasks.keys()): - if not all_tasks[dir_name].parent: - _print_task_tree(dir_name) - - if task_count == 0: - lines.append("(no active tasks)") - lines.append(f"Total: {task_count} active task(s)") - lines.append("") - - # My tasks - lines.append("## MY TASKS (Assigned to me)") - my_task_count = 0 - - for t in all_tasks.values(): - if t.assignee == developer and t.status != "done": - progress = children_progress(t.children, all_statuses) - lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress}") - my_task_count += 1 - - if my_task_count == 0: - lines.append("(no tasks assigned to you)") - lines.append("") - - # Journal file - lines.append("## JOURNAL FILE") - journal_file = get_active_journal_file(repo_root) - if journal_file: - journal_lines = count_lines(journal_file) - relative = f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}" - lines.append(f"Active file: {relative}") - lines.append(f"Line count: {journal_lines} / 2000") - if journal_lines > 1800: - lines.append("[!] WARNING: Approaching 2000 line limit!") - else: - lines.append("No journal file found") - lines.append("") - - # Packages - packages_text = get_packages_section(repo_root) - if packages_text: - lines.append(packages_text) - lines.append("") - - # Paths - lines.append("## PATHS") - lines.append(f"Workspace: {DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/") - lines.append(f"Tasks: {DIR_WORKFLOW}/{DIR_TASKS}/") - lines.append(f"Spec: {DIR_WORKFLOW}/{DIR_SPEC}/") - lines.append("") - - lines.append("========================================") - - return "\n".join(lines) - - -# ============================================================================= -# Record Mode -# ============================================================================= - -def get_context_record_json(repo_root: Path | None = None) -> dict: - """Get record-mode context as a dictionary. - - Focused on: my active tasks, git status, current task. - """ - if repo_root is None: - repo_root = get_repo_root() - - developer = get_developer(repo_root) - tasks_dir = get_tasks_dir(repo_root) - - root_git_info = _collect_root_git_info(repo_root) - - # My tasks (single pass — collect statuses and filter by assignee) - all_tasks_list = list(iter_active_tasks(tasks_dir)) - all_statuses = {t.dir_name: t.status for t in all_tasks_list} - - my_tasks = [] - for t in all_tasks_list: - if t.assignee == developer: - done = sum( - 1 for c in t.children - if all_statuses.get(c) in ("completed", "done") - ) - my_tasks.append({ - "dir": t.dir_name, - "title": t.title, - "status": t.status, - "priority": t.priority, - "children": list(t.children), - "childrenDone": done, - "parent": t.parent, - "meta": t.meta, - }) - - # Current task - current_task_info = None - current_task = get_current_task(repo_root) - if current_task: - source_type, context_key, _ = get_current_task_source(repo_root) - ct = load_task(repo_root / current_task) - if ct: - current_task_info = { - "path": current_task, - "name": ct.name, - "status": ct.status, - "source": source_type, - "contextKey": context_key, - } - - # Package git repos - pkg_git_info = _collect_package_git_info( - repo_root, - discover_unconfigured=not root_git_info["isRepo"], - ) - - result = { - "developer": developer or "", - "git": { - "isRepo": root_git_info["isRepo"], - "branch": root_git_info["branch"], - "isClean": root_git_info["isClean"], - "uncommittedChanges": root_git_info["uncommittedChanges"], - "recentCommits": root_git_info["recentCommits"], - }, - "myTasks": my_tasks, - "currentTask": current_task_info, - } - - if pkg_git_info: - result["packageGit"] = pkg_git_info - - return result - - -def get_context_text_record(repo_root: Path | None = None) -> str: - """Get context as formatted text for record-session mode. - - Focused output: MY ACTIVE TASKS first (with [!!!] emphasis), - then GIT STATUS, RECENT COMMITS, CURRENT TASK. - """ - if repo_root is None: - repo_root = get_repo_root() - - lines: list[str] = [] - lines.append("========================================") - lines.append("SESSION CONTEXT (RECORD MODE)") - lines.append("========================================") - lines.append("") - - developer = get_developer(repo_root) - if not developer: - lines.append( - f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>" - ) - return "\n".join(lines) - - # MY ACTIVE TASKS — first and prominent - lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})") - lines.append("[!] Review whether any should be archived before recording this session.") - lines.append("") - - tasks_dir = get_tasks_dir(repo_root) - my_task_count = 0 - - # Single pass — collect all tasks and filter by assignee - all_statuses = get_all_statuses(tasks_dir) - - for t in iter_active_tasks(tasks_dir): - if t.assignee == developer: - progress = children_progress(t.children, all_statuses) - lines.append(f"- [{t.priority}] {t.title} ({t.status}){progress} — {t.dir_name}") - my_task_count += 1 - - if my_task_count == 0: - lines.append("(no active tasks assigned to you)") - lines.append("") - - root_git_info = _collect_root_git_info(repo_root) - _append_root_git_context(lines, root_git_info) - - # Package git repos — independent sub-repositories - _append_package_git_context( - lines, - _collect_package_git_info( - repo_root, - discover_unconfigured=not root_git_info["isRepo"], - ), - ) - - # CURRENT TASK - lines.append("## CURRENT TASK") - current_task = get_current_task(repo_root) - if current_task: - source_type, context_key, _ = get_current_task_source(repo_root) - lines.append(f"Path: {current_task}") - lines.append( - f"Source: {source_type}" + (f":{context_key}" if context_key else "") - ) - ct = load_task(repo_root / current_task) - if ct: - lines.append(f"Name: {ct.name}") - lines.append(f"Status: {ct.status}") - else: - lines.append("(none)") - lines.append("") - - lines.append("========================================") - - return "\n".join(lines) - - -def output_text(repo_root: Path | None = None) -> None: - """Output context in text format. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - """ - if repo_root is None: - repo_root = get_repo_root() - update_hint = _get_update_hint(repo_root) - if update_hint: - print(update_hint) - print("") - print(get_context_text(repo_root)) diff --git a/.trellis/scripts/common/task_context.py b/.trellis/scripts/common/task_context.py deleted file mode 100755 index fa88412..0000000 --- a/.trellis/scripts/common/task_context.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python3 -""" -Task JSONL context management. - -Provides: - cmd_add_context - Add entry to JSONL context file - cmd_validate - Validate JSONL context files - cmd_list_context - List JSONL context entries - -Note: - ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files - are now seeded at ``task.py create`` time with a self-describing - ``_example`` line; the AI agent curates real entries during Phase 1.3 of - the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current - instructions. -""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path - -from .log import Colors, colored -from .paths import get_repo_root -from .task_utils import resolve_task_dir - - -# ============================================================================= -# Command: add-context -# ============================================================================= - -def cmd_add_context(args: argparse.Namespace) -> int: - """Add entry to JSONL context file.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - - jsonl_name = args.file - path = args.path - reason = args.reason or "Added manually" - - if not target_dir.is_dir(): - print(colored(f"Error: Directory not found: {target_dir}", Colors.RED)) - return 1 - - # Support shorthand - if not jsonl_name.endswith(".jsonl"): - jsonl_name = f"{jsonl_name}.jsonl" - - jsonl_file = target_dir / jsonl_name - full_path = repo_root / path - - entry_type = "file" - if full_path.is_dir(): - entry_type = "directory" - if not path.endswith("/"): - path = f"{path}/" - elif not full_path.is_file(): - print(colored(f"Error: Path not found: {path}", Colors.RED)) - return 1 - - # Check if already exists - if jsonl_file.is_file(): - content = jsonl_file.read_text(encoding="utf-8") - if f'"{path}"' in content: - print(colored(f"Warning: Entry already exists for {path}", Colors.YELLOW)) - return 0 - - # Add entry - entry: dict - if entry_type == "directory": - entry = {"file": path, "type": "directory", "reason": reason} - else: - entry = {"file": path, "reason": reason} - - with jsonl_file.open("a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") - - print(colored(f"Added {entry_type}: {path}", Colors.GREEN)) - return 0 - - -# ============================================================================= -# Command: validate -# ============================================================================= - -def cmd_validate(args: argparse.Namespace) -> int: - """Validate JSONL context files.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - - if not target_dir.is_dir(): - print(colored("Error: task directory required", Colors.RED)) - return 1 - - print(colored("=== Validating Context Files ===", Colors.BLUE)) - print(f"Target dir: {target_dir}") - print() - - total_errors = 0 - for jsonl_name in ["implement.jsonl", "check.jsonl"]: - jsonl_file = target_dir / jsonl_name - errors = _validate_jsonl(jsonl_file, repo_root) - total_errors += errors - - print() - if total_errors == 0: - print(colored("✓ All validations passed", Colors.GREEN)) - return 0 - else: - print(colored(f"✗ Validation failed ({total_errors} errors)", Colors.RED)) - return 1 - - -def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int: - """Validate a single JSONL file. - - Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are - skipped silently; they are self-describing comments, not real entries. - """ - file_name = jsonl_file.name - errors = 0 - - if not jsonl_file.is_file(): - print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}") - return 0 - - line_num = 0 - real_entries = 0 - for line in jsonl_file.read_text(encoding="utf-8").splitlines(): - line_num += 1 - if not line.strip(): - continue - - try: - data = json.loads(line) - except json.JSONDecodeError: - print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}") - errors += 1 - continue - - file_path = data.get("file") - entry_type = data.get("type", "file") - - if not file_path: - # Seed / comment row — skip silently - continue - - real_entries += 1 - full_path = repo_root / file_path - if entry_type == "directory": - if not full_path.is_dir(): - print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}") - errors += 1 - else: - if not full_path.is_file(): - print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}") - errors += 1 - - if errors == 0: - print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}") - else: - print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}") - - return errors - - -# ============================================================================= -# Command: list-context -# ============================================================================= - -def cmd_list_context(args: argparse.Namespace) -> int: - """List JSONL context entries.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - - if not target_dir.is_dir(): - print(colored("Error: task directory required", Colors.RED)) - return 1 - - print(colored("=== Context Files ===", Colors.BLUE)) - print() - - for jsonl_name in ["implement.jsonl", "check.jsonl"]: - jsonl_file = target_dir / jsonl_name - if not jsonl_file.is_file(): - continue - - print(colored(f"[{jsonl_name}]", Colors.CYAN)) - - count = 0 - seed_only = True - for line in jsonl_file.read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - - try: - data = json.loads(line) - except json.JSONDecodeError: - continue - - file_path = data.get("file") - if not file_path: - # Seed / comment row — don't count as a real entry - continue - seed_only = False - - count += 1 - entry_type = data.get("type", "file") - reason = data.get("reason", "-") - - if entry_type == "directory": - print(f" {colored(f'{count}.', Colors.GREEN)} [DIR] {file_path}") - else: - print(f" {colored(f'{count}.', Colors.GREEN)} {file_path}") - print(f" {colored('→', Colors.YELLOW)} {reason}") - - if seed_only: - print(f" {colored('(no curated entries yet — only seed row)', Colors.YELLOW)}") - - print() - - return 0 diff --git a/.trellis/scripts/common/task_queue.py b/.trellis/scripts/common/task_queue.py deleted file mode 100755 index f7485e2..0000000 --- a/.trellis/scripts/common/task_queue.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -""" -Task queue utility functions. - -Provides: - list_tasks_by_status - List tasks by status - list_pending_tasks - List tasks with pending status - list_tasks_by_assignee - List tasks by assignee - list_my_tasks - List tasks assigned to current developer - get_task_stats - Get P0/P1/P2/P3 counts -""" - -from __future__ import annotations - -from pathlib import Path - -from .paths import ( - get_repo_root, - get_developer, - get_tasks_dir, -) -from .tasks import iter_active_tasks - - -# ============================================================================= -# Internal helper -# ============================================================================= - -def _task_to_dict(t) -> dict: - """Convert TaskInfo to the dict format callers expect.""" - return { - "priority": t.priority, - "id": t.raw.get("id", ""), - "title": t.title, - "status": t.status, - "assignee": t.assignee or "-", - "dir": t.dir_name, - "children": list(t.children), - "parent": t.parent, - } - - -# ============================================================================= -# Public Functions -# ============================================================================= - -def list_tasks_by_status( - filter_status: str | None = None, - repo_root: Path | None = None -) -> list[dict]: - """List tasks by status. - - Args: - filter_status: Optional status filter. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - List of task info dicts with keys: priority, id, title, status, assignee. - """ - if repo_root is None: - repo_root = get_repo_root() - - tasks_dir = get_tasks_dir(repo_root) - results = [] - - for t in iter_active_tasks(tasks_dir): - if filter_status and t.status != filter_status: - continue - results.append(_task_to_dict(t)) - - return results - - -def list_pending_tasks(repo_root: Path | None = None) -> list[dict]: - """List pending tasks. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - List of task info dicts. - """ - return list_tasks_by_status("planning", repo_root) - - -def list_tasks_by_assignee( - assignee: str, - filter_status: str | None = None, - repo_root: Path | None = None -) -> list[dict]: - """List tasks assigned to a specific developer. - - Args: - assignee: Developer name. - filter_status: Optional status filter. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - List of task info dicts. - """ - if repo_root is None: - repo_root = get_repo_root() - - tasks_dir = get_tasks_dir(repo_root) - results = [] - - for t in iter_active_tasks(tasks_dir): - if (t.assignee or "-") != assignee: - continue - if filter_status and t.status != filter_status: - continue - results.append(_task_to_dict(t)) - - return results - - -def list_my_tasks( - filter_status: str | None = None, - repo_root: Path | None = None -) -> list[dict]: - """List tasks assigned to current developer. - - Args: - filter_status: Optional status filter. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - List of task info dicts. - - Raises: - ValueError: If developer not set. - """ - if repo_root is None: - repo_root = get_repo_root() - - developer = get_developer(repo_root) - if not developer: - raise ValueError("Developer not set") - - return list_tasks_by_assignee(developer, filter_status, repo_root) - - -def get_task_stats(repo_root: Path | None = None) -> dict[str, int]: - """Get task statistics. - - Args: - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Dict with keys: P0, P1, P2, P3, Total. - """ - if repo_root is None: - repo_root = get_repo_root() - - tasks_dir = get_tasks_dir(repo_root) - stats = {"P0": 0, "P1": 0, "P2": 0, "P3": 0, "Total": 0} - - for t in iter_active_tasks(tasks_dir): - if t.priority in stats: - stats[t.priority] += 1 - stats["Total"] += 1 - - return stats - - -def format_task_stats(stats: dict[str, int]) -> str: - """Format task stats as string. - - Args: - stats: Stats dict from get_task_stats. - - Returns: - Formatted string like "P0:0 P1:1 P2:2 P3:0 Total:3". - """ - return f"P0:{stats['P0']} P1:{stats['P1']} P2:{stats['P2']} P3:{stats['P3']} Total:{stats['Total']}" - - -# ============================================================================= -# Main Entry (for testing) -# ============================================================================= - -if __name__ == "__main__": - stats = get_task_stats() - print(format_task_stats(stats)) - print() - print("Pending tasks:") - for task in list_pending_tasks(): - print(f" {task['priority']}|{task['id']}|{task['title']}|{task['status']}|{task['assignee']}") diff --git a/.trellis/scripts/common/task_store.py b/.trellis/scripts/common/task_store.py deleted file mode 100755 index dfcd850..0000000 --- a/.trellis/scripts/common/task_store.py +++ /dev/null @@ -1,697 +0,0 @@ -#!/usr/bin/env python3 -""" -Task CRUD operations. - -Provides: - ensure_tasks_dir - Ensure tasks directory exists - cmd_create - Create a new task - cmd_archive - Archive completed task - cmd_set_branch - Set git branch for task - cmd_set_base_branch - Set PR target branch - cmd_set_scope - Set scope for PR title - cmd_add_subtask - Link child task to parent - cmd_remove_subtask - Unlink child task from parent -""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from datetime import datetime -from pathlib import Path - -from .config import ( - get_packages, - get_session_auto_commit, - is_monorepo, - resolve_package, - validate_package, -) -from .git import run_git -from .io import read_json, write_json -from .log import Colors, colored -from .paths import ( - DIR_ARCHIVE, - DIR_TASKS, - DIR_WORKFLOW, - FILE_TASK_JSON, - generate_task_date_prefix, - get_developer, - get_repo_root, - get_tasks_dir, -) -from .safe_commit import ( - print_gitignore_warning, - safe_archive_paths_to_add, - safe_git_add, -) -from .task_utils import ( - archive_task_complete, - find_task_by_name, - resolve_task_dir, - run_task_hooks, -) - - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def _slugify(title: str) -> str: - """Convert title to slug (only works with ASCII).""" - result = title.lower() - result = re.sub(r"[^a-z0-9]", "-", result) - result = re.sub(r"-+", "-", result) - result = result.strip("-") - return result - - -def ensure_tasks_dir(repo_root: Path) -> Path: - """Ensure tasks directory exists.""" - tasks_dir = get_tasks_dir(repo_root) - archive_dir = tasks_dir / "archive" - - if not tasks_dir.exists(): - tasks_dir.mkdir(parents=True) - print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr) - - if not archive_dir.exists(): - archive_dir.mkdir(parents=True) - - return tasks_dir - - -def _find_archived_task_by_dir_name(tasks_dir: Path, dir_name: str) -> Path | None: - """Find an archived task directory with the exact active-task dir name.""" - archive_dir = tasks_dir / DIR_ARCHIVE - if not archive_dir.is_dir(): - return None - - for month_dir in sorted(archive_dir.iterdir()): - if not month_dir.is_dir(): - continue - candidate = month_dir / dir_name - if candidate.is_dir(): - return candidate - - return None - - -def _repo_relative_path(path: Path, repo_root: Path) -> str: - """Format a path relative to the repo root when possible.""" - try: - return path.relative_to(repo_root).as_posix() - except ValueError: - return str(path) - - -# ============================================================================= -# Sub-agent platform detection + JSONL seeding -# ============================================================================= - -# Config directories of platforms that consume implement.jsonl / check.jsonl. -# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the -# platforms listed in workflow.md's "agent-capable" Skill Routing block -# (Class-1 hook-inject + Class-2 pull-based preludes). Kilo / Antigravity / -# Windsurf are NOT in this list: they do not consume JSONL. -_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = ( - ".claude", - ".cursor", - ".codex", - ".kiro", - ".gemini", - ".opencode", - ".qoder", - ".codebuddy", - ".factory", # Factory Droid - ".github/copilot", - ".pi", # Pi Agent -) - -_SEED_EXAMPLE = ( - "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. " - "Put spec/research files only — no code paths. " - "Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. " - "Delete this line once real entries are added." -) - - -def _has_subagent_platform(repo_root: Path) -> bool: - """Return True if any sub-agent-capable platform is configured. - - Detected by probing well-known config directories at the repo root. Used - only to decide whether ``task.py create`` should seed empty - ``implement.jsonl`` / ``check.jsonl`` files. - """ - for config_dir in _SUBAGENT_CONFIG_DIRS: - if (repo_root / config_dir).is_dir(): - return True - return False - - -def _write_seed_jsonl(path: Path) -> None: - """Write a one-line seed JSONL file with a self-describing ``_example``. - - The seed row has no ``file`` field, so downstream consumers (hooks + - preludes) that iterate entries via ``item.get("file")`` naturally skip - it. The row exists purely as an in-file prompt for the AI curator. - """ - seed = {"_example": _SEED_EXAMPLE} - path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8") - - -# ============================================================================= -# Command: create -# ============================================================================= - -def cmd_create(args: argparse.Namespace) -> int: - """Create a new task.""" - repo_root = get_repo_root() - - if not args.title: - print(colored("Error: title is required", Colors.RED), file=sys.stderr) - return 1 - - # Validate --package (CLI source: fail-fast) - package: str | None = getattr(args, "package", None) - if not is_monorepo(repo_root): - # Single-repo: ignore --package, no package prefix - if package: - print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr) - package = None - elif package: - if not validate_package(package, repo_root): - packages = get_packages(repo_root) - available = ", ".join(sorted(packages.keys())) if packages else "(none)" - print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr) - return 1 - else: - # Inferred: default_package → None (no task.json yet for create) - package = resolve_package(repo_root=repo_root) - - # Default assignee to current developer - assignee = args.assignee - if not assignee: - assignee = get_developer(repo_root) - if not assignee: - print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr) - return 1 - - ensure_tasks_dir(repo_root) - - # Get current developer as creator - creator = get_developer(repo_root) or assignee - - # Generate slug if not provided - slug = args.slug or _slugify(args.title) - if not slug: - print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr) - return 1 - - # Create task directory with MM-DD-slug format - tasks_dir = get_tasks_dir(repo_root) - date_prefix = generate_task_date_prefix() - dir_name = f"{date_prefix}-{slug}" - task_dir = tasks_dir / dir_name - task_json_path = task_dir / FILE_TASK_JSON - - archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, dir_name) - if archived_task_dir: - print(colored(f"Error: Task already archived: {dir_name}", Colors.RED), file=sys.stderr) - print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr) - print("Use a new slug if you intend to create a new task.", file=sys.stderr) - return 1 - - if task_dir.exists(): - print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr) - else: - task_dir.mkdir(parents=True) - - today = datetime.now().strftime("%Y-%m-%d") - - # Record current branch as base_branch (PR target) - _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root) - current_branch = branch_out.strip() or "main" - - task_data = { - "id": slug, - "name": slug, - "title": args.title, - "description": args.description or "", - "status": "planning", - "dev_type": None, - "scope": None, - "package": package, - "priority": args.priority, - "creator": creator, - "assignee": assignee, - "createdAt": today, - "completedAt": None, - "branch": None, - "base_branch": current_branch, - "worktree_path": None, - "commit": None, - "pr_url": None, - "subtasks": [], - "children": [], - "parent": None, - "relatedFiles": [], - "notes": "", - "meta": {}, - } - - write_json(task_json_path, task_data) - - # Seed implement.jsonl / check.jsonl for sub-agent-capable platforms. - # Agent curates real entries in Phase 1.3 (see .trellis/workflow.md). - # Agent-less platforms (Kilo / Antigravity / Windsurf) skip this — they - # load specs via the trellis-before-dev skill instead of JSONL. - seeded_jsonl = False - if _has_subagent_platform(repo_root): - for jsonl_name in ("implement.jsonl", "check.jsonl"): - jsonl_path = task_dir / jsonl_name - if not jsonl_path.exists(): - _write_seed_jsonl(jsonl_path) - seeded_jsonl = True - - # Handle --parent: establish bidirectional link - if args.parent: - parent_dir = resolve_task_dir(args.parent, repo_root) - parent_json_path = parent_dir / FILE_TASK_JSON - if not parent_json_path.is_file(): - print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr) - else: - parent_data = read_json(parent_json_path) - if parent_data: - # Add child to parent's children list - parent_children = parent_data.get("children", []) - if dir_name not in parent_children: - parent_children.append(dir_name) - parent_data["children"] = parent_children - write_json(parent_json_path, parent_data) - - # Set parent in child's task.json - task_data["parent"] = parent_dir.name - write_json(task_json_path, task_data) - - print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr) - - # Auto-activate the new task so the per-turn breadcrumb fires planning - # state. Best-effort: gracefully degrade if no session identity (CLI run - # outside an AI session) — the task is still created, the user can run - # task.py start later. Pointer is session-scoped so this never affects - # other AI sessions. - try: - from .active_task import resolve_context_key, set_active_task - if resolve_context_key(): - try: - rel_dir = task_dir.relative_to(repo_root).as_posix() - except ValueError: - rel_dir = str(task_dir) - set_active_task(rel_dir, repo_root) - except Exception: - pass - - print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr) - print("", file=sys.stderr) - print(colored("Next steps:", Colors.BLUE), file=sys.stderr) - print(" 1. Create prd.md with requirements", file=sys.stderr) - if seeded_jsonl: - print( - " 2. Curate implement.jsonl / check.jsonl (spec + research files only — " - "see .trellis/workflow.md Phase 1.3)", - file=sys.stderr, - ) - print(" 3. Run: python3 task.py start <dir>", file=sys.stderr) - else: - print(" 2. Run: python3 task.py start <dir>", file=sys.stderr) - print("", file=sys.stderr) - - # Output relative path for script chaining - print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}") - - run_task_hooks("after_create", task_json_path, repo_root) - return 0 - - -# ============================================================================= -# Command: archive -# ============================================================================= - -def cmd_archive(args: argparse.Namespace) -> int: - """Archive completed task.""" - repo_root = get_repo_root() - task_name = args.name - - if not task_name: - print(colored("Error: Task name is required", Colors.RED), file=sys.stderr) - return 1 - - tasks_dir = get_tasks_dir(repo_root) - - # Resolve task directory (supports task name, relative path, or absolute path) - task_dir = resolve_task_dir(task_name, repo_root) - - if not task_dir or not task_dir.is_dir(): - print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr) - print("Active tasks:", file=sys.stderr) - # Import lazily to avoid circular dependency - from .tasks import iter_active_tasks - for t in iter_active_tasks(tasks_dir): - print(f" - {t.dir_name}/", file=sys.stderr) - return 1 - - dir_name = task_dir.name - task_json_path = task_dir / FILE_TASK_JSON - - # Update status before archiving - today = datetime.now().strftime("%Y-%m-%d") - # Names of child task dirs whose task.json gets modified below; passed - # into safe_archive_paths_to_add so they're staged in this commit. - modified_children: list[str] = [] - if task_json_path.is_file(): - data = read_json(task_json_path) - if data: - data["status"] = "completed" - data["completedAt"] = today - write_json(task_json_path, data) - - # Handle subtask relationships on archive. - # Keep this task in its parent's children list so progress - # counters (children_progress) stay consistent — children - # missing from the active set are treated as completed. - task_children = data.get("children", []) - - # If this is a parent, clear parent field in all children - if task_children: - for child_name in task_children: - child_dir_path = find_task_by_name(child_name, tasks_dir) - if child_dir_path: - child_json = child_dir_path / FILE_TASK_JSON - if child_json.is_file(): - child_data = read_json(child_json) - if child_data: - child_data["parent"] = None - write_json(child_json, child_data) - modified_children.append(child_dir_path.name) - - # Clear any session that still points at this task before the path moves. - from .active_task import clear_task_from_sessions - clear_task_from_sessions(str(task_dir), repo_root) - - # Archive - result = archive_task_complete(task_dir, repo_root) - if "archived_to" in result: - archive_dest = Path(result["archived_to"]) - year_month = archive_dest.parent.name - print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr) - - # Auto-commit unless --no-commit - if not getattr(args, "no_commit", False): - _auto_commit_archive(dir_name, repo_root, modified_children) - - # Return the archive path - print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}") - - # Run hooks with the archived path - archived_json = archive_dest / FILE_TASK_JSON - run_task_hooks("after_archive", archived_json, repo_root) - return 0 - - return 1 - - -def _auto_commit_archive( - task_name: str, - repo_root: Path, - modified_children: list[str] | None = None, -) -> None: - """Stage Trellis-owned task paths and commit after archive. - - Scoped narrowly to the archived task's source + destination paths - plus any child task dirs whose ``task.json`` was edited (parent → - children relationship update). Dirty changes in OTHER active task - dirs are NOT bundled into the archive commit. - - If ``.gitignore`` blocks the paths, we warn + skip — we do NOT - retry with ``git add -f``. The warning explicitly forbids - ``git add -f .trellis/`` (which would fan out to caches/backups) - and points users at ``session_auto_commit: false``. - - Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when - set to ``false``, this function returns immediately without - touching git (the archive directory move on disk is unaffected). - """ - if not get_session_auto_commit(repo_root): - print( - "[OK] session_auto_commit: false — skipping git stage/commit.", - file=sys.stderr, - ) - return - - paths = safe_archive_paths_to_add( - repo_root, task_name=task_name, modified_children=modified_children - ) - if not paths: - print("[OK] No task changes to commit.", file=sys.stderr) - return - - success, _, err = safe_git_add(paths, repo_root) - if not success: - if err and "ignored by" in err.lower(): - print_gitignore_warning(paths) - else: - print( - f"[WARN] git add failed: {err.strip() if err else 'unknown error'}", - file=sys.stderr, - ) - return - - # Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses - # `git add` (no -A) which only stages additions/modifications. The - # source task directory was moved away by `shutil.move`, so its files - # need an explicit `git rm --cached` to stage the deletions in this - # same commit — otherwise they sit as uncommitted "phantom deletes" - # against HEAD until something later picks them up. - # - # `--ignore-unmatch` makes this a no-op when the task was never tracked - # (e.g. archiving a task that lived only in working tree). - source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}" - run_git( - ["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel], - cwd=repo_root, - ) - - rc, _, _ = run_git( - ["diff", "--cached", "--quiet", "--", *paths, source_rel], - cwd=repo_root, - ) - if rc == 0: - print("[OK] No task changes to commit.", file=sys.stderr) - return - - commit_msg = f"chore(task): archive {task_name}" - rc, _, err = run_git(["commit", "-m", commit_msg], cwd=repo_root) - if rc == 0: - print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr) - else: - print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr) - - -# ============================================================================= -# Command: add-subtask -# ============================================================================= - -def cmd_add_subtask(args: argparse.Namespace) -> int: - """Link a child task to a parent task.""" - repo_root = get_repo_root() - - parent_dir = resolve_task_dir(args.parent_dir, repo_root) - child_dir = resolve_task_dir(args.child_dir, repo_root) - - parent_json_path = parent_dir / FILE_TASK_JSON - child_json_path = child_dir / FILE_TASK_JSON - - if not parent_json_path.is_file(): - print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr) - return 1 - - if not child_json_path.is_file(): - print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr) - return 1 - - parent_data = read_json(parent_json_path) - child_data = read_json(child_json_path) - - if not parent_data or not child_data: - print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr) - return 1 - - # Check if child already has a parent - existing_parent = child_data.get("parent") - if existing_parent: - print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr) - return 1 - - # Add child to parent's children list - parent_children = parent_data.get("children", []) - child_dir_name = child_dir.name - if child_dir_name not in parent_children: - parent_children.append(child_dir_name) - parent_data["children"] = parent_children - - # Set parent in child's task.json - child_data["parent"] = parent_dir.name - - # Write both - write_json(parent_json_path, parent_data) - write_json(child_json_path, child_data) - - print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr) - return 0 - - -# ============================================================================= -# Command: remove-subtask -# ============================================================================= - -def cmd_remove_subtask(args: argparse.Namespace) -> int: - """Unlink a child task from a parent task.""" - repo_root = get_repo_root() - - parent_dir = resolve_task_dir(args.parent_dir, repo_root) - child_dir = resolve_task_dir(args.child_dir, repo_root) - - parent_json_path = parent_dir / FILE_TASK_JSON - child_json_path = child_dir / FILE_TASK_JSON - - if not parent_json_path.is_file(): - print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr) - return 1 - - if not child_json_path.is_file(): - print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr) - return 1 - - parent_data = read_json(parent_json_path) - child_data = read_json(child_json_path) - - if not parent_data or not child_data: - print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr) - return 1 - - # Remove child from parent's children list - parent_children = parent_data.get("children", []) - child_dir_name = child_dir.name - if child_dir_name in parent_children: - parent_children.remove(child_dir_name) - parent_data["children"] = parent_children - - # Clear parent in child's task.json - child_data["parent"] = None - - # Write both - write_json(parent_json_path, parent_data) - write_json(child_json_path, child_data) - - print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr) - return 0 - - -# ============================================================================= -# Command: set-branch -# ============================================================================= - -def cmd_set_branch(args: argparse.Namespace) -> int: - """Set git branch for task.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - branch = args.branch - - if not branch: - print(colored("Error: Missing arguments", Colors.RED)) - print("Usage: python3 task.py set-branch <task-dir> <branch-name>") - return 1 - - task_json = target_dir / FILE_TASK_JSON - if not task_json.is_file(): - print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) - return 1 - - data = read_json(task_json) - if not data: - return 1 - - data["branch"] = branch - write_json(task_json, data) - - print(colored(f"✓ Branch set to: {branch}", Colors.GREEN)) - return 0 - - -# ============================================================================= -# Command: set-base-branch -# ============================================================================= - -def cmd_set_base_branch(args: argparse.Namespace) -> int: - """Set the base branch (PR target) for task.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - base_branch = args.base_branch - - if not base_branch: - print(colored("Error: Missing arguments", Colors.RED)) - print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>") - print("Example: python3 task.py set-base-branch <dir> develop") - print() - print("This sets the target branch for PR (the branch your feature will merge into).") - return 1 - - task_json = target_dir / FILE_TASK_JSON - if not task_json.is_file(): - print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) - return 1 - - data = read_json(task_json) - if not data: - return 1 - - data["base_branch"] = base_branch - write_json(task_json, data) - - print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN)) - print(f" PR will target: {base_branch}") - return 0 - - -# ============================================================================= -# Command: set-scope -# ============================================================================= - -def cmd_set_scope(args: argparse.Namespace) -> int: - """Set scope for PR title.""" - repo_root = get_repo_root() - target_dir = resolve_task_dir(args.dir, repo_root) - scope = args.scope - - if not scope: - print(colored("Error: Missing arguments", Colors.RED)) - print("Usage: python3 task.py set-scope <task-dir> <scope>") - return 1 - - task_json = target_dir / FILE_TASK_JSON - if not task_json.is_file(): - print(colored(f"Error: task.json not found at {target_dir}", Colors.RED)) - return 1 - - data = read_json(task_json) - if not data: - return 1 - - data["scope"] = scope - write_json(task_json, data) - - print(colored(f"✓ Scope set to: {scope}", Colors.GREEN)) - return 0 diff --git a/.trellis/scripts/common/task_utils.py b/.trellis/scripts/common/task_utils.py deleted file mode 100755 index 62c215e..0000000 --- a/.trellis/scripts/common/task_utils.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 -""" -Task utility functions. - -Provides: - is_safe_task_path - Validate task path is safe to operate on - find_task_by_name - Find task directory by name - resolve_task_dir - Resolve task directory from name, relative, or absolute path - archive_task_dir - Archive task to monthly directory - run_task_hooks - Run lifecycle hooks for task events -""" - -from __future__ import annotations - -import shutil -import sys -from datetime import datetime -from pathlib import Path - -from .paths import get_repo_root, get_tasks_dir - - -# ============================================================================= -# Path Safety -# ============================================================================= - -def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool: - """Check if a relative task path is safe to operate on. - - Args: - task_path: Task path (relative to repo_root). - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - True if safe, False if dangerous. - """ - if repo_root is None: - repo_root = get_repo_root() - - normalized = task_path.replace("\\", "/") - - # Check empty or null - if not normalized or normalized == "null": - print("Error: empty or null task path", file=sys.stderr) - return False - - # Reject absolute paths - if Path(task_path).is_absolute(): - print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr) - return False - - # Reject ".", "..", paths starting with "./" or "../", or containing ".." - if normalized in (".", "..") or normalized.startswith("./") or normalized.startswith("../") or ".." in normalized: - print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr) - return False - - # Final check: ensure resolved path is not the repo root - abs_path = repo_root / Path(normalized) - if abs_path.exists(): - try: - resolved = abs_path.resolve() - root_resolved = repo_root.resolve() - if resolved == root_resolved: - print(f"Error: path resolves to repo root: {task_path}", file=sys.stderr) - return False - except (OSError, IOError): - pass - - return True - - -# ============================================================================= -# Task Lookup -# ============================================================================= - -def find_task_by_name(task_name: str, tasks_dir: Path) -> Path | None: - """Find task directory by name (exact or suffix match). - - Args: - task_name: Task name to find. - tasks_dir: Tasks directory path. - - Returns: - Absolute path to task directory, or None if not found. - """ - if not task_name or not tasks_dir or not tasks_dir.is_dir(): - return None - - # Try exact match first - exact_match = tasks_dir / task_name - if exact_match.is_dir(): - return exact_match - - # Try suffix match (e.g., "my-task" matches "01-21-my-task") - for d in tasks_dir.iterdir(): - if d.is_dir() and d.name.endswith(f"-{task_name}"): - return d - - return None - - -# ============================================================================= -# Archive Operations -# ============================================================================= - -def archive_task_dir(task_dir_abs: Path, repo_root: Path | None = None) -> Path | None: - """Archive a task directory to archive/{YYYY-MM}/. - - Args: - task_dir_abs: Absolute path to task directory. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Path to archived directory, or None on error. - """ - if not task_dir_abs.is_dir(): - print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr) - return None - - # Get tasks directory (parent of the task) - tasks_dir = task_dir_abs.parent - archive_dir = tasks_dir / "archive" - year_month = datetime.now().strftime("%Y-%m") - month_dir = archive_dir / year_month - - # Create archive directory - try: - month_dir.mkdir(parents=True, exist_ok=True) - except (OSError, IOError) as e: - print(f"Error: Failed to create archive directory: {e}", file=sys.stderr) - return None - - # Move task to archive - task_name = task_dir_abs.name - dest = month_dir / task_name - - try: - shutil.move(str(task_dir_abs), str(dest)) - except (OSError, IOError, shutil.Error) as e: - print(f"Error: Failed to move task to archive: {e}", file=sys.stderr) - return None - - return dest - - -def archive_task_complete( - task_dir_abs: Path, - repo_root: Path | None = None -) -> dict[str, str]: - """Complete archive workflow: archive directory. - - Args: - task_dir_abs: Absolute path to task directory. - repo_root: Repository root path. Defaults to auto-detected. - - Returns: - Dict with archive result info. - """ - if not task_dir_abs.is_dir(): - print(f"Error: task directory not found: {task_dir_abs}", file=sys.stderr) - return {} - - archive_dest = archive_task_dir(task_dir_abs, repo_root) - if archive_dest: - return {"archived_to": str(archive_dest)} - - return {} - - -# ============================================================================= -# Task Directory Resolution -# ============================================================================= - -def resolve_task_dir(target_dir: str, repo_root: Path) -> Path: - """Resolve task directory to absolute path. - - Supports: - - Absolute path: /path/to/task - - Relative path: .trellis/tasks/01-31-my-task - - Task name: my-task (uses find_task_by_name for lookup) - - Args: - target_dir: Task directory specification. - repo_root: Repository root path. - - Returns: - Resolved absolute path. - """ - if not target_dir: - return Path() - - normalized = target_dir.replace("\\", "/") - while normalized.startswith("./"): - normalized = normalized[2:] - - # Absolute path - if Path(target_dir).is_absolute(): - return Path(target_dir) - - # Relative path (contains path separator or starts with .trellis) - if "/" in normalized or normalized.startswith(".trellis"): - return repo_root / Path(normalized) - - # Task name - try to find in tasks directory - tasks_dir = get_tasks_dir(repo_root) - found = find_task_by_name(target_dir, tasks_dir) - if found: - return found - - # Fallback to treating as relative path - return repo_root / Path(normalized) - - -# ============================================================================= -# Lifecycle Hooks -# ============================================================================= - -def run_task_hooks(event: str, task_json_path: Path, repo_root: Path) -> None: - """Run lifecycle hooks for a task event. - - Args: - event: Event name (e.g. "after_create"). - task_json_path: Absolute path to the task's task.json. - repo_root: Repository root for cwd and config lookup. - """ - import os - import subprocess - - from .config import get_hooks - from .log import Colors, colored - - commands = get_hooks(event, repo_root) - if not commands: - return - - env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)} - - for cmd in commands: - try: - result = subprocess.run( - cmd, - shell=True, - cwd=repo_root, - env=env, - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - if result.returncode != 0: - print( - colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW), - file=sys.stderr, - ) - if result.stderr.strip(): - print(f" {result.stderr.strip()}", file=sys.stderr) - except Exception as e: - print( - colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW), - file=sys.stderr, - ) - - -# ============================================================================= -# Main Entry (for testing) -# ============================================================================= - -if __name__ == "__main__": - repo = get_repo_root() - tasks = get_tasks_dir(repo) - - print(f"Tasks dir: {tasks}") - print(f"is_safe_task_path('.trellis/tasks/test'): {is_safe_task_path('.trellis/tasks/test', repo)}") - print(f"is_safe_task_path('../test'): {is_safe_task_path('../test', repo)}") diff --git a/.trellis/scripts/common/tasks.py b/.trellis/scripts/common/tasks.py deleted file mode 100755 index 7b44094..0000000 --- a/.trellis/scripts/common/tasks.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Task data access layer. - -Single source of truth for loading and iterating task directories. -Replaces scattered task.json parsing across 9+ files. - -Provides: - load_task — Load a single task by directory path - iter_active_tasks — Iterate all non-archived tasks (sorted) - get_all_statuses — Get {dir_name: status} map for children progress -""" - -from __future__ import annotations - -from collections.abc import Iterator -from pathlib import Path - -from .io import read_json -from .paths import FILE_TASK_JSON -from .types import TaskInfo - - -def load_task(task_dir: Path) -> TaskInfo | None: - """Load task from a directory containing task.json. - - Args: - task_dir: Absolute path to the task directory. - - Returns: - TaskInfo if task.json exists and is valid, None otherwise. - """ - task_json = task_dir / FILE_TASK_JSON - if not task_json.is_file(): - return None - - data = read_json(task_json) - if not data: - return None - - return TaskInfo( - dir_name=task_dir.name, - directory=task_dir, - title=data.get("title") or data.get("name") or "unknown", - status=data.get("status", "unknown"), - assignee=data.get("assignee", ""), - priority=data.get("priority", "P2"), - children=tuple(data.get("children", [])), - parent=data.get("parent"), - package=data.get("package"), - raw=data, - ) - - -def iter_active_tasks(tasks_dir: Path) -> Iterator[TaskInfo]: - """Iterate all active (non-archived) tasks, sorted by directory name. - - Skips the "archive" directory and directories without valid task.json. - - Args: - tasks_dir: Path to the tasks directory. - - Yields: - TaskInfo for each valid task. - """ - if not tasks_dir.is_dir(): - return - - for d in sorted(tasks_dir.iterdir()): - if not d.is_dir() or d.name == "archive": - continue - info = load_task(d) - if info is not None: - yield info - - -def get_all_statuses(tasks_dir: Path) -> dict[str, str]: - """Get a {dir_name: status} mapping for all active tasks. - - Useful for computing children progress without loading full TaskInfo. - - Args: - tasks_dir: Path to the tasks directory. - - Returns: - Dict mapping directory names to status strings. - """ - return {t.dir_name: t.status for t in iter_active_tasks(tasks_dir)} - - -def children_progress( - children: tuple[str, ...] | list[str], - all_statuses: dict[str, str], -) -> str: - """Format children progress string like " [2/3 done]". - - Args: - children: List of child directory names. - all_statuses: Status map from get_all_statuses(). - - Returns: - Formatted string, or "" if no children. - """ - if not children: - return "" - # A child missing from active statuses has been archived (cmd_archive - # sets status=completed before moving the dir). Count it as done so - # parent progress doesn't regress when children are archived. - done = sum( - 1 for c in children - if c not in all_statuses or all_statuses.get(c) in ("completed", "done") - ) - return f" [{done}/{len(children)} done]" diff --git a/.trellis/scripts/common/trellis_config.py b/.trellis/scripts/common/trellis_config.py deleted file mode 100755 index 5dbec7a..0000000 --- a/.trellis/scripts/common/trellis_config.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone reader for .trellis/config.yaml. - -Mirrors a minimal subset of common.config so callers (hooks, workflow_phase) -can read configuration without importing the full task/repo helpers. Returns -an empty dict on missing/malformed files so callers stay simple. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Optional - - -CONFIG_REL_PATH = ".trellis/config.yaml" - - -def _unquote(value: str) -> str: - if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): - return value[1:-1] - return value - - -def _strip_inline_comment(value: str) -> str: - """Strip ` # …` inline comments while preserving `#` inside quoted strings. - - YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token - is part of the value. Quoted strings are immune. - """ - in_quote: str | None = None - for idx, ch in enumerate(value): - if in_quote: - if ch == in_quote: - in_quote = None - continue - if ch in ('"', "'"): - in_quote = ch - continue - if ch == "#" and (idx == 0 or value[idx - 1].isspace()): - return value[:idx] - return value - - -def _next_content_line(lines: list[str], start: int) -> tuple[int, str]: - i = start - while i < len(lines): - stripped = lines[i].strip() - if stripped and not stripped.startswith("#"): - return i, lines[i] - i += 1 - return i, "" - - -def _parse_yaml_block( - lines: list[str], start: int, min_indent: int, target: dict -) -> int: - i = start - current_list: list | None = None - - while i < len(lines): - line = lines[i] - stripped = line.strip() - - if not stripped or stripped.startswith("#"): - i += 1 - continue - - indent = len(line) - len(line.lstrip()) - if indent < min_indent: - break - - if stripped.startswith("- "): - if current_list is not None: - current_list.append(_unquote(stripped[2:].strip())) - i += 1 - elif ":" in stripped: - key, _, value = stripped.partition(":") - key = key.strip() - value = _strip_inline_comment(value).strip() - value = _unquote(value) - current_list = None - - if value: - target[key] = value - i += 1 - else: - next_i, next_line = _next_content_line(lines, i + 1) - if next_i >= len(lines): - target[key] = {} - i = next_i - elif next_line.strip().startswith("- "): - current_list = [] - target[key] = current_list - i += 1 - else: - next_indent = len(next_line) - len(next_line.lstrip()) - if next_indent > indent: - nested: dict = {} - target[key] = nested - i = _parse_yaml_block(lines, i + 1, next_indent, nested) - else: - target[key] = {} - i += 1 - else: - i += 1 - - return i - - -def parse_simple_yaml(content: str) -> dict: - """Parse a small subset of YAML. See common.config for full doc.""" - lines = content.splitlines() - result: dict = {} - _parse_yaml_block(lines, 0, 0, result) - return result - - -def read_trellis_config(repo_root: Optional[Path] = None) -> dict: - """Read .trellis/config.yaml. Returns {} on missing or malformed file.""" - root = repo_root or Path.cwd() - config_file = root / CONFIG_REL_PATH - try: - content = config_file.read_text(encoding="utf-8") - except (FileNotFoundError, OSError): - return {} - try: - parsed = parse_simple_yaml(content) - except Exception: - return {} - return parsed if isinstance(parsed, dict) else {} diff --git a/.trellis/scripts/common/types.py b/.trellis/scripts/common/types.py deleted file mode 100755 index 5802e10..0000000 --- a/.trellis/scripts/common/types.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Core type definitions for Trellis task data. - -Provides: - TaskData — TypedDict for task.json shape (read-path type hints only) - TaskInfo — Frozen dataclass for loaded task (the public API type) - AgentRecord — TypedDict for registry.json agent entries -""" - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import TypedDict - - -# ============================================================================= -# task.json shape (TypedDict — used only for read-path type hints) -# ============================================================================= - -class TaskData(TypedDict, total=False): - """Shape of task.json on disk. - - Used only for type annotations when reading task.json. - Writes must use the original dict to avoid losing unknown fields. - """ - - id: str - name: str - title: str - description: str - status: str - dev_type: str - scope: str | None - package: str | None - priority: str - creator: str - assignee: str - createdAt: str - completedAt: str | None - branch: str | None - base_branch: str | None - worktree_path: str | None - commit: str | None - pr_url: str | None - subtasks: list[str] - children: list[str] - parent: str | None - relatedFiles: list[str] - notes: str - meta: dict - - -# ============================================================================= -# Loaded task object (frozen dataclass — the public API type) -# ============================================================================= - -@dataclass(frozen=True) -class TaskInfo: - """Immutable view of a loaded task. - - Created by load_task() / iter_active_tasks(). - Contains the commonly accessed fields; the original dict - is preserved in `raw` for write-back and uncommon field access. - """ - - dir_name: str - directory: Path - title: str - status: str - assignee: str - priority: str - children: tuple[str, ...] - parent: str | None - package: str | None - raw: dict # original dict — use for writes and uncommon fields - - @property - def name(self) -> str: - """Task name (id or name field).""" - return self.raw.get("name") or self.raw.get("id") or self.dir_name - - @property - def description(self) -> str: - return self.raw.get("description", "") - - @property - def branch(self) -> str | None: - return self.raw.get("branch") - - @property - def meta(self) -> dict: - return self.raw.get("meta", {}) - - -# ============================================================================= -# registry.json agent entry -# ============================================================================= - -class AgentRecord(TypedDict, total=False): - """Shape of an agent entry in registry.json.""" - - id: str - pid: int - task_dir: str - worktree_path: str - branch: str - platform: str - started_at: str - status: str diff --git a/.trellis/scripts/common/workflow_phase.py b/.trellis/scripts/common/workflow_phase.py deleted file mode 100755 index 2b4acd0..0000000 --- a/.trellis/scripts/common/workflow_phase.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Workflow Phase Extraction. - -Extracts step-level content from .trellis/workflow.md and optionally filters -platform-specific blocks. - -Platform marker syntax in workflow.md: - - [Claude Code, Cursor, ...] - agent-capable content - [/Claude Code, Cursor, ...] - -Provides: - get_phase_index - Extract the Phase Index section (no --step) - get_step - Extract a single step (#### X.X) section - filter_platform - Strip platform blocks that don't include the given name -""" - -from __future__ import annotations - -import re - -from .paths import DIR_WORKFLOW, get_repo_root - - -def _workflow_md_path(): - return get_repo_root() / DIR_WORKFLOW / "workflow.md" - -# Match a line that *is* a platform marker: "[A, B, C]" or "[/A, B, C]" -_MARKER_RE = re.compile(r"^\[(/?)([A-Za-z][^\[\]]*)\]\s*$") - -# Step heading: "#### 1.0 Title" or "#### 1.0 ..." -_STEP_HEADING_RE = re.compile(r"^####\s+(\d+\.\d+)\b.*$") - -# Phase Index starts here; Phase 1/2/3 step bodies follow; ends at Breadcrumbs. -_PHASE_INDEX_HEADING = "## Phase Index" - - -def _read_workflow() -> str: - path = _workflow_md_path() - if not path.exists(): - raise FileNotFoundError(f"workflow.md not found: {path}") - return path.read_text(encoding="utf-8") - - -def _parse_marker(line: str) -> tuple[bool, list[str]] | None: - """Parse a platform marker line. - - Returns: - (is_closing, [platform_names]) if line is a marker, else None. - """ - m = _MARKER_RE.match(line) - if not m: - return None - is_closing = m.group(1) == "/" - names = [p.strip() for p in m.group(2).split(",") if p.strip()] - return is_closing, names - - -def get_phase_index() -> str: - """Return Phase Index + Phase 1/2/3 step bodies from workflow.md. - - Matches what the SessionStart hook injects into the `<workflow>` block: - starts at `## Phase Index`, continues through `## Phase 1: Plan`, - `## Phase 2: Execute`, `## Phase 3: Finish`, stops at - `## Customizing Trellis (for forks)` (the docs-for-forks footer). - `[workflow-state:STATUS]` tag blocks (now embedded in Phase Index since - v0.5.0-rc.0) are consumed by the UserPromptSubmit hook so they're - stripped from this output. - """ - text = _read_workflow() - lines = text.splitlines() - - start: int | None = None - end: int | None = None - for i, line in enumerate(lines): - stripped = line.strip() - if start is None and stripped == _PHASE_INDEX_HEADING: - start = i - continue - if start is not None and stripped == "## Customizing Trellis (for forks)": - end = i - break - - if start is None: - return "" - if end is None: - end = len(lines) - - section = "\n".join(lines[start:end]).rstrip() - # Strip [workflow-state:STATUS]...[/workflow-state:STATUS] blocks since - # they're injected separately by inject-workflow-state.py per-turn. - import re as _re - tag_re = _re.compile( - r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]\n?", - _re.DOTALL, - ) - return tag_re.sub("", section).rstrip() + "\n" - - -def get_step(step_id: str) -> str: - """Return the `#### X.X` section matching step_id (header + body). - - Body ends at the next `####` or `---` or `##` heading (whichever comes first). - """ - text = _read_workflow() - lines = text.splitlines() - - start: int | None = None - for i, line in enumerate(lines): - m = _STEP_HEADING_RE.match(line) - if m and m.group(1) == step_id: - start = i - break - if start is None: - return "" - - end: int = len(lines) - for j in range(start + 1, len(lines)): - line = lines[j] - if line.startswith("#### "): - end = j - break - if line.startswith("## "): - end = j - break - # Horizontal rule at column 0 - if line.strip() == "---": - end = j - break - - return "\n".join(lines[start:end]).rstrip() + "\n" - - -def _platform_matches(platform: str, block_names: list[str]) -> bool: - """Case-insensitive fuzzy match: accept 'cursor', 'Cursor', 'claude-code', 'Claude Code'.""" - needle = platform.lower().replace("-", "").replace("_", "").replace(" ", "") - for name in block_names: - hay = name.lower().replace("-", "").replace("_", "").replace(" ", "") - if needle == hay: - return True - return False - - -def resolve_effective_platform(platform: str, config: dict) -> str: - """Map ``codex`` to a dispatch-mode-namespaced virtual platform name. - - When ``--platform codex`` is passed, return ``"codex-inline"`` (default) - or ``"codex-sub-agent"`` based on ``.trellis/config.yaml`` ``codex.dispatch_mode``. - ``filter_platform`` then surfaces blocks whose marker lists include the - namespaced name (e.g. ``[codex-sub-agent, ...]`` or ``[codex-inline, Kilo, - Antigravity, Windsurf]``). - - Default is ``inline`` because Codex sub-agents run with ``fork_turns="none"`` - isolation and can't inherit the parent session's task context — inline - keeps the main agent in charge so context isn't lost. Invalid / missing - values also fall back to inline. - - Other platforms are returned unchanged. - """ - if platform == "codex": - mode = "inline" - codex_cfg = config.get("codex") if isinstance(config, dict) else None - if isinstance(codex_cfg, dict): - cfg_mode = codex_cfg.get("dispatch_mode") - if cfg_mode in ("inline", "sub-agent"): - mode = cfg_mode - return f"codex-{mode}" - return platform - - -def filter_platform(content: str, platform: str) -> str: - """Keep lines outside any `[...]` block + lines inside blocks that include platform. - - Marker lines themselves are dropped from the output. - """ - lines = content.splitlines() - out: list[str] = [] - - in_block = False - keep_block = False - - for line in lines: - marker = _parse_marker(line) - if marker is not None: - is_closing, names = marker - if not is_closing: - in_block = True - keep_block = _platform_matches(platform, names) - else: - in_block = False - keep_block = False - continue # drop the marker line itself - - if in_block: - if keep_block: - out.append(line) - continue - out.append(line) - - # Collapse runs of 3+ blank lines that may arise from dropped markers - collapsed: list[str] = [] - blank_run = 0 - for line in out: - if line.strip() == "": - blank_run += 1 - if blank_run <= 2: - collapsed.append(line) - else: - blank_run = 0 - collapsed.append(line) - - return "\n".join(collapsed).rstrip() + "\n" diff --git a/.trellis/scripts/get_context.py b/.trellis/scripts/get_context.py deleted file mode 100755 index bc63463..0000000 --- a/.trellis/scripts/get_context.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -""" -Get Session Context for AI Agent. - -Usage: - python3 get_context.py Output context in text format - python3 get_context.py --json Output context in JSON format -""" - -from __future__ import annotations - -from common.git_context import main - - -if __name__ == "__main__": - main() diff --git a/.trellis/scripts/get_developer.py b/.trellis/scripts/get_developer.py deleted file mode 100755 index f8a89eb..0000000 --- a/.trellis/scripts/get_developer.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -""" -Get current developer name. - -This is a wrapper that uses common/paths.py -""" - -from __future__ import annotations - -import sys - -from common.paths import get_developer - - -def main() -> None: - """CLI entry point.""" - developer = get_developer() - if developer: - print(developer) - else: - print("Developer not initialized", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.trellis/scripts/hooks/linear_sync.py b/.trellis/scripts/hooks/linear_sync.py deleted file mode 100755 index 5659fde..0000000 --- a/.trellis/scripts/hooks/linear_sync.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -"""Linear sync hook for Trellis task lifecycle. - -Syncs task events to Linear via the `linearis` CLI. - -Usage (called automatically by task.py hooks): - python3 .trellis/scripts/hooks/linear_sync.py create - python3 .trellis/scripts/hooks/linear_sync.py start - python3 .trellis/scripts/hooks/linear_sync.py archive - -Manual usage: - TASK_JSON_PATH=.trellis/tasks/<name>/task.json python3 .trellis/scripts/hooks/linear_sync.py sync - -Environment: - TASK_JSON_PATH - Absolute path to task.json (set by task.py) - -Configuration: - .trellis/hooks.local.json - Local config (gitignored), example: - { - "linear": { - "team": "TEAM_KEY", - "project": "Project Name", - "assignees": { - "dev-name": "linear-user-id" - } - } - } -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - -# ─── Configuration ──────────────────────────────────────────────────────────── - -# Trellis priority → Linear priority (1=Urgent, 2=High, 3=Medium, 4=Low) -PRIORITY_MAP = {"P0": 1, "P1": 2, "P2": 3, "P3": 4} - -# Linear status names (must match your team's workflow) -STATUS_IN_PROGRESS = "In Progress" -STATUS_DONE = "Done" - - -def _load_config() -> dict: - """Load local hook config from .trellis/hooks.local.json.""" - task_json_path = os.environ.get("TASK_JSON_PATH", "") - if task_json_path: - # Walk up from task.json to find .trellis/ - trellis_dir = Path(task_json_path).parent.parent.parent - else: - trellis_dir = Path(".trellis") - - config_path = trellis_dir / "hooks.local.json" - try: - with open(config_path, encoding="utf-8") as f: - return json.load(f) - except (OSError, json.JSONDecodeError): - return {} - - -CONFIG = _load_config() -LINEAR_CFG = CONFIG.get("linear", {}) - -TEAM = LINEAR_CFG.get("team", "") -PROJECT = LINEAR_CFG.get("project", "") -ASSIGNEE_MAP = LINEAR_CFG.get("assignees", {}) - -# ─── Helpers ────────────────────────────────────────────────────────────────── - - -def _read_task() -> tuple[dict, str]: - path = os.environ.get("TASK_JSON_PATH", "") - if not path: - print("TASK_JSON_PATH not set", file=sys.stderr) - sys.exit(1) - with open(path, encoding="utf-8") as f: - return json.load(f), path - - -def _write_task(data: dict, path: str) -> None: - with open(path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - f.write("\n") - - -def _linearis(*args: str) -> dict | None: - result = subprocess.run( - ["linearis", *args], - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - ) - if result.returncode != 0: - print(f"linearis error: {result.stderr.strip()}", file=sys.stderr) - sys.exit(1) - stdout = result.stdout.strip() - if stdout: - return json.loads(stdout) - return None - - -def _get_linear_issue(task: dict) -> str | None: - meta = task.get("meta") - if isinstance(meta, dict): - return meta.get("linear_issue") - return None - - -# ─── Actions ────────────────────────────────────────────────────────────────── - - -def cmd_create() -> None: - if not TEAM: - print("No linear.team configured in hooks.local.json", file=sys.stderr) - sys.exit(1) - - task, path = _read_task() - - # Skip if already linked - if _get_linear_issue(task): - print(f"Already linked: {_get_linear_issue(task)}") - return - - title = task.get("title") or task.get("name") or "Untitled" - args = ["issues", "create", title, "--team", TEAM] - - # Map priority - priority = PRIORITY_MAP.get(task.get("priority", ""), 0) - if priority: - args.extend(["-p", str(priority)]) - - # Set project - if PROJECT: - args.extend(["--project", PROJECT]) - - # Assign to Linear user - assignee = task.get("assignee", "") - linear_user_id = ASSIGNEE_MAP.get(assignee) - if linear_user_id: - args.extend(["--assignee", linear_user_id]) - - # Link to parent's Linear issue if available - parent_issue = _resolve_parent_linear_issue(task) - if parent_issue: - args.extend(["--parent-ticket", parent_issue]) - - result = _linearis(*args) - if result and "identifier" in result: - if not isinstance(task.get("meta"), dict): - task["meta"] = {} - task["meta"]["linear_issue"] = result["identifier"] - _write_task(task, path) - print(f"Created Linear issue: {result['identifier']}") - - -def cmd_start() -> None: - task, _ = _read_task() - issue = _get_linear_issue(task) - if not issue: - return - _linearis("issues", "update", issue, "-s", STATUS_IN_PROGRESS) - print(f"Updated {issue} -> {STATUS_IN_PROGRESS}") - cmd_sync() - - -def cmd_archive() -> None: - task, _ = _read_task() - issue = _get_linear_issue(task) - if not issue: - return - _linearis("issues", "update", issue, "-s", STATUS_DONE) - print(f"Updated {issue} -> {STATUS_DONE}") - - -def cmd_sync() -> None: - """Sync prd.md content to Linear issue description.""" - task, _ = _read_task() - issue = _get_linear_issue(task) - if not issue: - print("No linear_issue in meta, run create first", file=sys.stderr) - sys.exit(1) - - # Find prd.md next to task.json - task_json_path = os.environ.get("TASK_JSON_PATH", "") - prd_path = Path(task_json_path).parent / "prd.md" - if not prd_path.is_file(): - print(f"No prd.md found at {prd_path}", file=sys.stderr) - sys.exit(1) - - description = prd_path.read_text(encoding="utf-8").strip() - _linearis("issues", "update", issue, "-d", description) - print(f"Synced prd.md to {issue} description") - - -# ─── Parent Issue Resolution ───────────────────────────────────────────────── - - -def _resolve_parent_linear_issue(task: dict) -> str | None: - """Find parent task's Linear issue identifier.""" - parent_name = task.get("parent") - if not parent_name: - return None - - task_json_path = os.environ.get("TASK_JSON_PATH", "") - if not task_json_path: - return None - - current_task_dir = Path(task_json_path).parent - tasks_dir = current_task_dir.parent - parent_json = tasks_dir / parent_name / "task.json" - - if parent_json.exists(): - try: - with open(parent_json, encoding="utf-8") as f: - parent_task = json.load(f) - return _get_linear_issue(parent_task) - except (json.JSONDecodeError, OSError): - pass - return None - - -# ─── Main ───────────────────────────────────────────────────────────────────── - -if __name__ == "__main__": - action = sys.argv[1] if len(sys.argv) > 1 else "" - actions = { - "create": cmd_create, - "start": cmd_start, - "archive": cmd_archive, - "sync": cmd_sync, - } - fn = actions.get(action) - if fn: - fn() - else: - print(f"Unknown action: {action}", file=sys.stderr) - print(f"Valid actions: {', '.join(actions)}", file=sys.stderr) - sys.exit(1) diff --git a/.trellis/scripts/init_developer.py b/.trellis/scripts/init_developer.py deleted file mode 100755 index 9fb53f5..0000000 --- a/.trellis/scripts/init_developer.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -""" -Initialize developer for workflow. - -Usage: - python3 init_developer.py <developer-name> - -This creates: - - .trellis/.developer file with developer info - - .trellis/workspace/<name>/ directory structure -""" - -from __future__ import annotations - -import sys - -from common.paths import ( - DIR_WORKFLOW, - FILE_DEVELOPER, - get_developer, -) -from common.developer import init_developer - - -def main() -> None: - """CLI entry point.""" - if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} <developer-name>") - print() - print("Example:") - print(f" {sys.argv[0]} john") - sys.exit(1) - - name = sys.argv[1] - - # Check if already initialized - existing = get_developer() - if existing: - print(f"Developer already initialized: {existing}") - print() - print(f"To reinitialize, remove {DIR_WORKFLOW}/{FILE_DEVELOPER} first") - sys.exit(0) - - if init_developer(name): - sys.exit(0) - else: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.trellis/scripts/task.py b/.trellis/scripts/task.py deleted file mode 100755 index a3493bd..0000000 --- a/.trellis/scripts/task.py +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Task Management Script. - -Usage: - python3 task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>] - python3 task.py add-context <dir> <file> <path> [reason] # Add jsonl entry - python3 task.py validate <dir> # Validate jsonl files - python3 task.py list-context <dir> # List jsonl entries - python3 task.py start <dir> # Set active task - python3 task.py current [--source] # Show active task - python3 task.py finish # Clear active task - python3 task.py set-branch <dir> <branch> # Set git branch - python3 task.py set-base-branch <dir> <branch> # Set PR target branch - python3 task.py set-scope <dir> <scope> # Set scope for PR title - python3 task.py archive <task-dir> # Archive completed task - python3 task.py list # List active tasks - python3 task.py list-archive [month] # List archived tasks - python3 task.py add-subtask <parent-dir> <child-dir> # Link child to parent - python3 task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent -""" - -from __future__ import annotations - -import argparse -import sys - -from common.log import Colors, colored -from common.paths import ( - DIR_WORKFLOW, - DIR_TASKS, - FILE_TASK_JSON, - get_repo_root, - get_developer, - get_tasks_dir, - get_current_task, -) -from common.active_task import ( - clear_active_task, - resolve_active_task, - resolve_context_key, - set_active_task, -) -from common.io import read_json, write_json -from common.task_utils import resolve_task_dir, run_task_hooks -from common.tasks import iter_active_tasks, children_progress - -# Import command handlers from split modules (also re-exports for plan.py compatibility) -from common.task_store import ( - cmd_create, - cmd_archive, - cmd_set_branch, - cmd_set_base_branch, - cmd_set_scope, - cmd_add_subtask, - cmd_remove_subtask, -) -from common.task_context import ( - cmd_add_context, - cmd_validate, - cmd_list_context, -) - - -# ============================================================================= -# Command: start / finish -# ============================================================================= - -def cmd_start(args: argparse.Namespace) -> int: - """Set active task.""" - repo_root = get_repo_root() - task_input = args.dir - - if not task_input: - print(colored("Error: task directory or name required", Colors.RED)) - return 1 - - # Resolve task directory (supports task name, relative path, or absolute path) - full_path = resolve_task_dir(task_input, repo_root) - - if not full_path.is_dir(): - print(colored(f"Error: Task not found: {task_input}", Colors.RED)) - print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')") - return 1 - - # Convert to relative path for storage - try: - task_dir = full_path.relative_to(repo_root).as_posix() - except ValueError: - task_dir = str(full_path) - - task_json_path = full_path / FILE_TASK_JSON - - if not resolve_context_key(): - # Degraded mode: no session identity available. - # Hook didn't inject TRELLIS_CONTEXT_ID (common on Windows + Claude Code, - # --continue resume path, fork distribution, hooks disabled, etc.). Skip - # per-session pointer write; AI continues based on conversation context. - print(colored( - "ℹ Session identity not available; active-task pointer not persisted " - "this session (degraded mode). AI continues based on conversation context.", - Colors.YELLOW, - )) - print(colored( - "Hint: run inside an AI IDE/session that exposes session identity, " - "or set TRELLIS_CONTEXT_ID before running task.py start.", - Colors.YELLOW, - )) - - # Still flip task.json status: planning → in_progress so downstream phases proceed. - if task_json_path.is_file(): - data = read_json(task_json_path) - if data and data.get("status") == "planning": - data["status"] = "in_progress" - if write_json(task_json_path, data): - print(colored("✓ Status: planning → in_progress (degraded)", Colors.GREEN)) - run_task_hooks("after_start", task_json_path, repo_root) - return 0 - - active = set_active_task(task_dir, repo_root) - if active: - print(colored(f"✓ Current task set to: {task_dir}", Colors.GREEN)) - print(f"Source: {active.source}") - - if task_json_path.is_file(): - data = read_json(task_json_path) - if data and data.get("status") == "planning": - data["status"] = "in_progress" - if write_json(task_json_path, data): - print(colored("✓ Status: planning → in_progress", Colors.GREEN)) - - print() - print(colored("The hook will now inject context from this task's jsonl files.", Colors.BLUE)) - - run_task_hooks("after_start", task_json_path, repo_root) - return 0 - else: - print(colored("Error: Failed to set current task", Colors.RED)) - return 1 - - -def cmd_finish(args: argparse.Namespace) -> int: - """Clear active task.""" - repo_root = get_repo_root() - active = clear_active_task(repo_root) - current = active.task_path - - if not current: - print(colored("No current task set", Colors.YELLOW)) - return 0 - - # Resolve task.json path before clearing - task_json_path = repo_root / current / FILE_TASK_JSON - - print(colored(f"✓ Cleared current task (was: {current})", Colors.GREEN)) - print(f"Source: {active.source}") - - if task_json_path.is_file(): - run_task_hooks("after_finish", task_json_path, repo_root) - return 0 - - -def cmd_current(args: argparse.Namespace) -> int: - """Show active task.""" - repo_root = get_repo_root() - active = resolve_active_task(repo_root) - - if args.source: - print(f"Current task: {active.task_path or '(none)'}") - print(f"Source: {active.source}") - if active.stale: - print("State: stale") - return 0 if active.task_path else 1 - - if active.task_path: - print(active.task_path) - return 0 - - return 1 - - -# ============================================================================= -# Command: list -# ============================================================================= - -def cmd_list(args: argparse.Namespace) -> int: - """List active tasks.""" - repo_root = get_repo_root() - tasks_dir = get_tasks_dir(repo_root) - current_task = get_current_task(repo_root) - developer = get_developer(repo_root) - filter_mine = args.mine - filter_status = args.status - - if filter_mine: - if not developer: - print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr) - return 1 - print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE)) - else: - print(colored("All active tasks:", Colors.BLUE)) - print() - - # Single pass: collect all tasks via shared iterator - all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)} - all_statuses = {name: t.status for name, t in all_tasks.items()} - - # Display tasks hierarchically - count = 0 - - def _print_task(dir_name: str, indent: int = 0) -> None: - nonlocal count - t = all_tasks[dir_name] - - # Apply --mine filter - if filter_mine and (t.assignee or "-") != developer: - return - - # Apply --status filter - if filter_status and t.status != filter_status: - return - - relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}" - marker = "" - if relative_path == current_task: - marker = f" {colored('<- current', Colors.GREEN)}" - - # Children progress - progress = children_progress(t.children, all_statuses) - - # Package tag - pkg_tag = f" @{t.package}" if t.package else "" - - prefix = " " * indent + " - " - - if filter_mine: - print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress}{marker}") - else: - print(f"{prefix}{dir_name}/ ({t.status}){pkg_tag}{progress} [{colored(t.assignee or '-', Colors.CYAN)}]{marker}") - count += 1 - - # Print children indented - for child_name in t.children: - if child_name in all_tasks: - _print_task(child_name, indent + 1) - - # Display only top-level tasks (those without a parent) - for dir_name in sorted(all_tasks.keys()): - if not all_tasks[dir_name].parent: - _print_task(dir_name) - - if count == 0: - if filter_mine: - print(" (no tasks assigned to you)") - else: - print(" (no active tasks)") - - print() - print(f"Total: {count} task(s)") - return 0 - - -# ============================================================================= -# Command: list-archive -# ============================================================================= - -def cmd_list_archive(args: argparse.Namespace) -> int: - """List archived tasks.""" - repo_root = get_repo_root() - tasks_dir = get_tasks_dir(repo_root) - archive_dir = tasks_dir / "archive" - month = args.month - - print(colored("Archived tasks:", Colors.BLUE)) - print() - - if month: - month_dir = archive_dir / month - if month_dir.is_dir(): - print(f"[{month}]") - for d in sorted(month_dir.iterdir()): - if d.is_dir(): - print(f" - {d.name}/") - else: - print(f" No archives for {month}") - else: - if archive_dir.is_dir(): - for month_dir in sorted(archive_dir.iterdir()): - if month_dir.is_dir(): - month_name = month_dir.name - count = sum(1 for d in month_dir.iterdir() if d.is_dir()) - print(f"[{month_name}] - {count} task(s)") - - return 0 - - -# ============================================================================= -# Help -# ============================================================================= - -def show_usage() -> None: - """Show usage help.""" - print("""Task Management Script - -Usage: - python3 task.py create <title> Create new task directory - python3 task.py create <title> --package <pkg> Create task for a specific package - python3 task.py create <title> --parent <dir> Create task as child of parent - python3 task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl - python3 task.py validate <dir> Validate jsonl files - python3 task.py list-context <dir> List jsonl entries - python3 task.py start <dir> Set active task - python3 task.py current [--source] Show active task - python3 task.py finish Clear active task - python3 task.py set-branch <dir> <branch> Set git branch - python3 task.py set-base-branch <dir> <branch> Set PR target branch - python3 task.py set-scope <dir> <scope> Set scope for PR title - python3 task.py archive <task-dir> Archive completed task - python3 task.py add-subtask <parent> <child> Link child task to parent - python3 task.py remove-subtask <parent> <child> Unlink child from parent - python3 task.py list [--mine] [--status <status>] List tasks - python3 task.py list-archive [YYYY-MM] List archived tasks - -Monorepo options: - --package <pkg> Package name (validated against config.yaml packages) - -List options: - --mine, -m Show only tasks assigned to current developer - --status, -s <s> Filter by status (planning, in_progress, review, completed) - -Examples: - python3 task.py create "Add login feature" --slug add-login - python3 task.py create "Add login feature" --slug add-login --package cli - python3 task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent - python3 task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines" - python3 task.py set-branch <dir> task/add-login - python3 task.py start .trellis/tasks/01-21-add-login - python3 task.py current --source - python3 task.py finish - python3 task.py archive add-login - python3 task.py add-subtask parent-task child-task # Link existing tasks - python3 task.py remove-subtask parent-task child-task - python3 task.py list # List all active tasks - python3 task.py list --mine # List my tasks only - python3 task.py list --mine --status in_progress # List my in-progress tasks -""") - - -# ============================================================================= -# Main Entry -# ============================================================================= - -def main() -> int: - """CLI entry point.""" - # Deprecation guard: `init-context` was removed in v0.5.0-beta.12. - # Detect early so argparse doesn't mask the real reason with a generic - # "invalid choice" error. - if len(sys.argv) >= 2 and sys.argv[1] == "init-context": - print( - colored( - "Error: `task.py init-context` was removed in v0.5.0-beta.12.", - Colors.RED, - ), - file=sys.stderr, - ) - print( - "implement.jsonl / check.jsonl are now seeded on `task.py create` for", - file=sys.stderr, - ) - print( - "sub-agent-capable platforms and curated by the AI during Phase 1.3.", - file=sys.stderr, - ) - print("See .trellis/workflow.md Phase 1.3 or run:", file=sys.stderr) - print( - " python3 ./.trellis/scripts/get_context.py --mode phase --step 1.3", - file=sys.stderr, - ) - print( - "Use `task.py add-context <dir> implement|check <path> <reason>` to append entries.", - file=sys.stderr, - ) - return 2 - - parser = argparse.ArgumentParser( - description="Task Management Script", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - subparsers = parser.add_subparsers(dest="command", help="Commands") - - # create - p_create = subparsers.add_parser("create", help="Create new task") - p_create.add_argument("title", help="Task title") - p_create.add_argument("--slug", "-s", help="Task slug") - p_create.add_argument("--assignee", "-a", help="Assignee developer") - p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)") - p_create.add_argument("--description", "-d", help="Task description") - p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)") - p_create.add_argument("--package", help="Package name for monorepo projects") - - # add-context - p_add = subparsers.add_parser("add-context", help="Add context entry") - p_add.add_argument("dir", help="Task directory") - p_add.add_argument("file", help="JSONL file (implement|check)") - p_add.add_argument("path", help="File path to add") - p_add.add_argument("reason", nargs="?", help="Reason for adding") - - # validate - p_validate = subparsers.add_parser("validate", help="Validate context files") - p_validate.add_argument("dir", help="Task directory") - - # list-context - p_listctx = subparsers.add_parser("list-context", help="List context entries") - p_listctx.add_argument("dir", help="Task directory") - - # start - p_start = subparsers.add_parser("start", help="Set active task") - p_start.add_argument("dir", help="Task directory") - - # current - p_current = subparsers.add_parser("current", help="Show active task") - p_current.add_argument("--source", action="store_true", - help="Show active task source") - - # finish - subparsers.add_parser("finish", help="Clear active task") - - # set-branch - p_branch = subparsers.add_parser("set-branch", help="Set git branch") - p_branch.add_argument("dir", help="Task directory") - p_branch.add_argument("branch", help="Branch name") - - # set-base-branch - p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch") - p_base.add_argument("dir", help="Task directory") - p_base.add_argument("base_branch", help="Base branch name (PR target)") - - # set-scope - p_scope = subparsers.add_parser("set-scope", help="Set scope") - p_scope.add_argument("dir", help="Task directory") - p_scope.add_argument("scope", help="Scope name") - - # archive - p_archive = subparsers.add_parser("archive", help="Archive task") - p_archive.add_argument("name", help="Task directory or name") - p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive") - - # list - p_list = subparsers.add_parser("list", help="List tasks") - p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only") - p_list.add_argument("--status", "-s", help="Filter by status") - - # add-subtask - p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent") - p_addsub.add_argument("parent_dir", help="Parent task directory") - p_addsub.add_argument("child_dir", help="Child task directory") - - # remove-subtask - p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent") - p_rmsub.add_argument("parent_dir", help="Parent task directory") - p_rmsub.add_argument("child_dir", help="Child task directory") - - # list-archive - p_listarch = subparsers.add_parser("list-archive", help="List archived tasks") - p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)") - - args = parser.parse_args() - - if not args.command: - show_usage() - return 1 - - commands = { - "create": cmd_create, - "add-context": cmd_add_context, - "validate": cmd_validate, - "list-context": cmd_list_context, - "start": cmd_start, - "current": cmd_current, - "finish": cmd_finish, - "set-branch": cmd_set_branch, - "set-base-branch": cmd_set_base_branch, - "set-scope": cmd_set_scope, - "archive": cmd_archive, - "add-subtask": cmd_add_subtask, - "remove-subtask": cmd_remove_subtask, - "list": cmd_list, - "list-archive": cmd_list_archive, - } - - if args.command in commands: - return commands[args.command](args) - else: - show_usage() - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.trellis/spec/backend/database-guidelines.md b/.trellis/spec/backend/database-guidelines.md deleted file mode 100644 index b61aa78..0000000 --- a/.trellis/spec/backend/database-guidelines.md +++ /dev/null @@ -1,51 +0,0 @@ -# Database Guidelines - -> Database patterns and conventions for this project. - ---- - -## Overview - -<!-- -Document your project's database conventions here. - -Questions to answer: -- What ORM/query library do you use? -- How are migrations managed? -- What are the naming conventions for tables/columns? -- How do you handle transactions? ---> - -(To be filled by the team) - ---- - -## Query Patterns - -<!-- How should queries be written? Batch operations? --> - -(To be filled by the team) - ---- - -## Migrations - -<!-- How to create and run migrations --> - -(To be filled by the team) - ---- - -## Naming Conventions - -<!-- Table names, column names, index names --> - -(To be filled by the team) - ---- - -## Common Mistakes - -<!-- Database-related mistakes your team has made --> - -(To be filled by the team) diff --git a/.trellis/spec/backend/directory-structure.md b/.trellis/spec/backend/directory-structure.md deleted file mode 100644 index 9bb253d..0000000 --- a/.trellis/spec/backend/directory-structure.md +++ /dev/null @@ -1,54 +0,0 @@ -# Directory Structure - -> How backend code is organized in this project. - ---- - -## Overview - -<!-- -Document your project's backend directory structure here. - -Questions to answer: -- How are modules/packages organized? -- Where does business logic live? -- Where are API endpoints defined? -- How are utilities and helpers organized? ---> - -(To be filled by the team) - ---- - -## Directory Layout - -``` -<!-- Replace with your actual structure --> -src/ -├── ... -└── ... -``` - ---- - -## Module Organization - -<!-- How should new features/modules be organized? --> - -(To be filled by the team) - ---- - -## Naming Conventions - -<!-- File and folder naming rules --> - -(To be filled by the team) - ---- - -## Examples - -<!-- Link to well-organized modules as examples --> - -(To be filled by the team) diff --git a/.trellis/spec/backend/error-handling.md b/.trellis/spec/backend/error-handling.md deleted file mode 100644 index bcd5533..0000000 --- a/.trellis/spec/backend/error-handling.md +++ /dev/null @@ -1,51 +0,0 @@ -# Error Handling - -> How errors are handled in this project. - ---- - -## Overview - -<!-- -Document your project's error handling conventions here. - -Questions to answer: -- What error types do you define? -- How are errors propagated? -- How are errors logged? -- How are errors returned to clients? ---> - -(To be filled by the team) - ---- - -## Error Types - -<!-- Custom error classes/types --> - -(To be filled by the team) - ---- - -## Error Handling Patterns - -<!-- Try-catch patterns, error propagation --> - -(To be filled by the team) - ---- - -## API Error Responses - -<!-- Standard error response format --> - -(To be filled by the team) - ---- - -## Common Mistakes - -<!-- Error handling mistakes your team has made --> - -(To be filled by the team) diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md deleted file mode 100644 index 1c0b4c4..0000000 --- a/.trellis/spec/backend/index.md +++ /dev/null @@ -1,38 +0,0 @@ -# Backend Development Guidelines - -> Best practices for backend development in this project. - ---- - -## Overview - -This directory contains guidelines for backend development. Fill in each file with your project's specific conventions. - ---- - -## Guidelines Index - -| Guide | Description | Status | -|-------|-------------|--------| -| [Directory Structure](./directory-structure.md) | Module organization and file layout | To fill | -| [Database Guidelines](./database-guidelines.md) | ORM patterns, queries, migrations | To fill | -| [Error Handling](./error-handling.md) | Error types, handling strategies | To fill | -| [Quality Guidelines](./quality-guidelines.md) | Code standards, forbidden patterns | To fill | -| [Logging Guidelines](./logging-guidelines.md) | Structured logging, log levels | To fill | - ---- - -## How to Fill These Guidelines - -For each guideline file: - -1. Document your project's **actual conventions** (not ideals) -2. Include **code examples** from your codebase -3. List **forbidden patterns** and why -4. Add **common mistakes** your team has made - -The goal is to help AI assistants and new team members understand how YOUR project works. - ---- - -**Language**: All documentation should be written in **English**. diff --git a/.trellis/spec/backend/logging-guidelines.md b/.trellis/spec/backend/logging-guidelines.md deleted file mode 100644 index bb930df..0000000 --- a/.trellis/spec/backend/logging-guidelines.md +++ /dev/null @@ -1,51 +0,0 @@ -# Logging Guidelines - -> How logging is done in this project. - ---- - -## Overview - -<!-- -Document your project's logging conventions here. - -Questions to answer: -- What logging library do you use? -- What are the log levels and when to use each? -- What should be logged? -- What should NOT be logged (PII, secrets)? ---> - -(To be filled by the team) - ---- - -## Log Levels - -<!-- When to use each level: debug, info, warn, error --> - -(To be filled by the team) - ---- - -## Structured Logging - -<!-- Log format, required fields --> - -(To be filled by the team) - ---- - -## What to Log - -<!-- Important events to log --> - -(To be filled by the team) - ---- - -## What NOT to Log - -<!-- Sensitive data, PII, secrets --> - -(To be filled by the team) diff --git a/.trellis/spec/backend/quality-guidelines.md b/.trellis/spec/backend/quality-guidelines.md deleted file mode 100644 index c1e1065..0000000 --- a/.trellis/spec/backend/quality-guidelines.md +++ /dev/null @@ -1,51 +0,0 @@ -# Quality Guidelines - -> Code quality standards for backend development. - ---- - -## Overview - -<!-- -Document your project's quality standards here. - -Questions to answer: -- What patterns are forbidden? -- What linting rules do you enforce? -- What are your testing requirements? -- What code review standards apply? ---> - -(To be filled by the team) - ---- - -## Forbidden Patterns - -<!-- Patterns that should never be used and why --> - -(To be filled by the team) - ---- - -## Required Patterns - -<!-- Patterns that must always be used --> - -(To be filled by the team) - ---- - -## Testing Requirements - -<!-- What level of testing is expected --> - -(To be filled by the team) - ---- - -## Code Review Checklist - -<!-- What reviewers should check --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/component-guidelines.md b/.trellis/spec/frontend/component-guidelines.md deleted file mode 100644 index 6836c3f..0000000 --- a/.trellis/spec/frontend/component-guidelines.md +++ /dev/null @@ -1,59 +0,0 @@ -# Component Guidelines - -> How components are built in this project. - ---- - -## Overview - -<!-- -Document your project's component conventions here. - -Questions to answer: -- What component patterns do you use? -- How are props defined? -- How do you handle composition? -- What accessibility standards apply? ---> - -(To be filled by the team) - ---- - -## Component Structure - -<!-- Standard structure of a component file --> - -(To be filled by the team) - ---- - -## Props Conventions - -<!-- How props should be defined and typed --> - -(To be filled by the team) - ---- - -## Styling Patterns - -<!-- How styles are applied (CSS modules, styled-components, Tailwind, etc.) --> - -(To be filled by the team) - ---- - -## Accessibility - -<!-- A11y requirements and patterns --> - -(To be filled by the team) - ---- - -## Common Mistakes - -<!-- Component-related mistakes your team has made --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/directory-structure.md b/.trellis/spec/frontend/directory-structure.md deleted file mode 100644 index 1eb57d1..0000000 --- a/.trellis/spec/frontend/directory-structure.md +++ /dev/null @@ -1,54 +0,0 @@ -# Directory Structure - -> How frontend code is organized in this project. - ---- - -## Overview - -<!-- -Document your project's frontend directory structure here. - -Questions to answer: -- Where do components live? -- How are features/modules organized? -- Where are shared utilities? -- How are assets organized? ---> - -(To be filled by the team) - ---- - -## Directory Layout - -``` -<!-- Replace with your actual structure --> -src/ -├── ... -└── ... -``` - ---- - -## Module Organization - -<!-- How should new features be organized? --> - -(To be filled by the team) - ---- - -## Naming Conventions - -<!-- File and folder naming rules --> - -(To be filled by the team) - ---- - -## Examples - -<!-- Link to well-organized modules as examples --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/hook-guidelines.md b/.trellis/spec/frontend/hook-guidelines.md deleted file mode 100644 index 60c6bb6..0000000 --- a/.trellis/spec/frontend/hook-guidelines.md +++ /dev/null @@ -1,51 +0,0 @@ -# Hook Guidelines - -> How hooks are used in this project. - ---- - -## Overview - -<!-- -Document your project's hook conventions here. - -Questions to answer: -- What custom hooks do you have? -- How do you handle data fetching? -- What are the naming conventions? -- How do you share stateful logic? ---> - -(To be filled by the team) - ---- - -## Custom Hook Patterns - -<!-- How to create and structure custom hooks --> - -(To be filled by the team) - ---- - -## Data Fetching - -<!-- How data fetching is handled (React Query, SWR, etc.) --> - -(To be filled by the team) - ---- - -## Naming Conventions - -<!-- Hook naming rules (use*, etc.) --> - -(To be filled by the team) - ---- - -## Common Mistakes - -<!-- Hook-related mistakes your team has made --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/index.md b/.trellis/spec/frontend/index.md deleted file mode 100644 index a5e51b8..0000000 --- a/.trellis/spec/frontend/index.md +++ /dev/null @@ -1,39 +0,0 @@ -# Frontend Development Guidelines - -> Best practices for frontend development in this project. - ---- - -## Overview - -This directory contains guidelines for frontend development. Fill in each file with your project's specific conventions. - ---- - -## Guidelines Index - -| Guide | Description | Status | -|-------|-------------|--------| -| [Directory Structure](./directory-structure.md) | Module organization and file layout | To fill | -| [Component Guidelines](./component-guidelines.md) | Component patterns, props, composition | To fill | -| [Hook Guidelines](./hook-guidelines.md) | Custom hooks, data fetching patterns | To fill | -| [State Management](./state-management.md) | Local state, global state, server state | To fill | -| [Quality Guidelines](./quality-guidelines.md) | Code standards, forbidden patterns | To fill | -| [Type Safety](./type-safety.md) | Type patterns, validation | To fill | - ---- - -## How to Fill These Guidelines - -For each guideline file: - -1. Document your project's **actual conventions** (not ideals) -2. Include **code examples** from your codebase -3. List **forbidden patterns** and why -4. Add **common mistakes** your team has made - -The goal is to help AI assistants and new team members understand how YOUR project works. - ---- - -**Language**: All documentation should be written in **English**. diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md deleted file mode 100644 index 05a1411..0000000 --- a/.trellis/spec/frontend/quality-guidelines.md +++ /dev/null @@ -1,51 +0,0 @@ -# Quality Guidelines - -> Code quality standards for frontend development. - ---- - -## Overview - -<!-- -Document your project's quality standards here. - -Questions to answer: -- What patterns are forbidden? -- What linting rules do you enforce? -- What are your testing requirements? -- What code review standards apply? ---> - -(To be filled by the team) - ---- - -## Forbidden Patterns - -<!-- Patterns that should never be used and why --> - -(To be filled by the team) - ---- - -## Required Patterns - -<!-- Patterns that must always be used --> - -(To be filled by the team) - ---- - -## Testing Requirements - -<!-- What level of testing is expected --> - -(To be filled by the team) - ---- - -## Code Review Checklist - -<!-- What reviewers should check --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/state-management.md b/.trellis/spec/frontend/state-management.md deleted file mode 100644 index b4fc966..0000000 --- a/.trellis/spec/frontend/state-management.md +++ /dev/null @@ -1,51 +0,0 @@ -# State Management - -> How state is managed in this project. - ---- - -## Overview - -<!-- -Document your project's state management conventions here. - -Questions to answer: -- What state management solution do you use? -- How is local vs global state decided? -- How do you handle server state? -- What are the patterns for derived state? ---> - -(To be filled by the team) - ---- - -## State Categories - -<!-- Local state, global state, server state, URL state --> - -(To be filled by the team) - ---- - -## When to Use Global State - -<!-- Criteria for promoting state to global --> - -(To be filled by the team) - ---- - -## Server State - -<!-- How server data is cached and synchronized --> - -(To be filled by the team) - ---- - -## Common Mistakes - -<!-- State management mistakes your team has made --> - -(To be filled by the team) diff --git a/.trellis/spec/frontend/type-safety.md b/.trellis/spec/frontend/type-safety.md deleted file mode 100644 index 1b1b19e..0000000 --- a/.trellis/spec/frontend/type-safety.md +++ /dev/null @@ -1,51 +0,0 @@ -# Type Safety - -> Type safety patterns in this project. - ---- - -## Overview - -<!-- -Document your project's type safety conventions here. - -Questions to answer: -- What type system do you use? -- How are types organized? -- What validation library do you use? -- How do you handle type inference? ---> - -(To be filled by the team) - ---- - -## Type Organization - -<!-- Where types are defined, shared types vs local types --> - -(To be filled by the team) - ---- - -## Validation - -<!-- Runtime validation patterns (Zod, Yup, io-ts, etc.) --> - -(To be filled by the team) - ---- - -## Common Patterns - -<!-- Type utilities, generics, type guards --> - -(To be filled by the team) - ---- - -## Forbidden Patterns - -<!-- any, type assertions, etc. --> - -(To be filled by the team) diff --git a/.trellis/spec/guides/code-reuse-thinking-guide.md b/.trellis/spec/guides/code-reuse-thinking-guide.md deleted file mode 100644 index f9d5f99..0000000 --- a/.trellis/spec/guides/code-reuse-thinking-guide.md +++ /dev/null @@ -1,105 +0,0 @@ -# Code Reuse Thinking Guide - -> **Purpose**: Stop and think before creating new code - does it already exist? - ---- - -## The Problem - -**Duplicated code is the #1 source of inconsistency bugs.** - -When you copy-paste or rewrite existing logic: -- Bug fixes don't propagate -- Behavior diverges over time -- Codebase becomes harder to understand - ---- - -## Before Writing New Code - -### Step 1: Search First - -```bash -# Search for similar function names -grep -r "functionName" . - -# Search for similar logic -grep -r "keyword" . -``` - -### Step 2: Ask These Questions - -| Question | If Yes... | -|----------|-----------| -| Does a similar function exist? | Use or extend it | -| Is this pattern used elsewhere? | Follow the existing pattern | -| Could this be a shared utility? | Create it in the right place | -| Am I copying code from another file? | **STOP** - extract to shared | - ---- - -## Common Duplication Patterns - -### Pattern 1: Copy-Paste Functions - -**Bad**: Copying a validation function to another file - -**Good**: Extract to shared utilities, import where needed - -### Pattern 2: Similar Components - -**Bad**: Creating a new component that's 80% similar to existing - -**Good**: Extend existing component with props/variants - -### Pattern 3: Repeated Constants - -**Bad**: Defining the same constant in multiple files - -**Good**: Single source of truth, import everywhere - ---- - -## When to Abstract - -**Abstract when**: -- Same code appears 3+ times -- Logic is complex enough to have bugs -- Multiple people might need this - -**Don't abstract when**: -- Only used once -- Trivial one-liner -- Abstraction would be more complex than duplication - ---- - -## After Batch Modifications - -When you've made similar changes to multiple files: - -1. **Review**: Did you catch all instances? -2. **Search**: Run grep to find any missed -3. **Consider**: Should this be abstracted? - ---- - -## Gotcha: Asymmetric Mechanisms Producing Same Output - -**Problem**: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual `files.set()` for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts. - -**Symptom**: Init works perfectly, but update creates files at wrong paths or misses files entirely. - -**Prevention checklist**: -- [ ] When migrating directory structures, search for ALL code paths that reference the old structure -- [ ] If one path is auto-derived (glob/copy) and another is manually listed, the manual one needs updating -- [ ] Add a regression test that compares outputs from both mechanisms - ---- - -## Checklist Before Commit - -- [ ] Searched for existing similar code -- [ ] No copy-pasted logic that should be shared -- [ ] Constants defined in one place -- [ ] Similar patterns follow same structure diff --git a/.trellis/spec/guides/cross-layer-thinking-guide.md b/.trellis/spec/guides/cross-layer-thinking-guide.md deleted file mode 100644 index 0a91f11..0000000 --- a/.trellis/spec/guides/cross-layer-thinking-guide.md +++ /dev/null @@ -1,162 +0,0 @@ -# Cross-Layer Thinking Guide - -> **Purpose**: Think through data flow across layers before implementing. - ---- - -## The Problem - -**Most bugs happen at layer boundaries**, not within layers. - -Common cross-layer bugs: -- API returns format A, frontend expects format B -- Database stores X, service transforms to Y, but loses data -- Multiple layers implement the same logic differently - ---- - -## Before Implementing Cross-Layer Features - -### Step 1: Map the Data Flow - -Draw out how data moves: - -``` -Source → Transform → Store → Retrieve → Transform → Display -``` - -For each arrow, ask: -- What format is the data in? -- What could go wrong? -- Who is responsible for validation? - -### Step 2: Identify Boundaries - -| Boundary | Common Issues | -|----------|---------------| -| API ↔ Service | Type mismatches, missing fields | -| Service ↔ Database | Format conversions, null handling | -| Backend ↔ Frontend | Serialization, date formats | -| Component ↔ Component | Props shape changes | - -### Step 3: Define Contracts - -For each boundary: -- What is the exact input format? -- What is the exact output format? -- What errors can occur? - ---- - -## Common Cross-Layer Mistakes - -### Mistake 1: Implicit Format Assumptions - -**Bad**: Assuming date format without checking - -**Good**: Explicit format conversion at boundaries - -### Mistake 2: Scattered Validation - -**Bad**: Validating the same thing in multiple layers - -**Good**: Validate once at the entry point - -### Mistake 3: Leaky Abstractions - -**Bad**: Component knows about database schema - -**Good**: Each layer only knows its neighbors - ---- - -## Checklist for Cross-Layer Features - -Before implementation: -- [ ] Mapped the complete data flow -- [ ] Identified all layer boundaries -- [ ] Defined format at each boundary -- [ ] Decided where validation happens - -After implementation: -- [ ] Tested with edge cases (null, empty, invalid) -- [ ] Verified error handling at each boundary -- [ ] Checked data survives round-trip - ---- - -## Cross-Platform Template Consistency - -In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary. - -### Checklist: After Modifying Any Command Template - -- [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name "<command>.*"` -- [ ] Update all platform copies (Markdown `.md` and TOML `.toml`) -- [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings -- [ ] Run `/trellis:check-cross-layer` to verify nothing was missed - -**Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check. - ---- - -## Generated Runtime Template Upgrade Consistency - -Some generated files are both documentation and runtime input. In Trellis, -`.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`, -SessionStart filters, and per-turn hooks. Template changes must be validated -against both fresh init and upgrade paths. - -### Checklist: After Modifying A Runtime-Parsed Template - -- [ ] Identify every runtime parser that reads the template, not just the file - writer that installs it -- [ ] Check whether relevant syntax lives outside obvious managed regions - such as tag blocks -- [ ] Verify fresh `init` output and a versioned `update` scenario that writes - the older `.trellis/.version` -- [ ] Add an upgrade regression using an older pristine template fixture, then - assert the installed file reaches the current packaged shape -- [ ] Update the backend spec that owns the runtime contract - -**Real-world example**: Codex inline mode changed workflow platform markers from -`[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` / -`[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but -`trellis update` only merged `[workflow-state:*]` blocks and preserved stale -markers outside those blocks. Result: upgraded projects got new hook scripts -but old workflow routing, so `get_context.py --mode phase --platform codex` -could return empty Phase 2.1 detail. - ---- - -## Mode-Detection Probe Checklist - -When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download): - -### Before implementing: -- [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos) -- [ ] 404 vs transient error are distinguished — don't treat both as "not found" -- [ ] Transient errors **abort or retry**, never silently switch modes -- [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source) -- [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers - -### After implementing: -- [ ] Trace every path from probe result to the mode-decision branch — no fallthrough -- [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments -- [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON -- [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`) -- [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters - -**Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found". - -**Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail. - ---- - -## When to Create Flow Documentation - -Create detailed flow docs when: -- Feature spans 3+ layers -- Multiple teams are involved -- Data format is complex -- Feature has caused bugs before diff --git a/.trellis/spec/guides/index.md b/.trellis/spec/guides/index.md deleted file mode 100644 index 147c79b..0000000 --- a/.trellis/spec/guides/index.md +++ /dev/null @@ -1,79 +0,0 @@ -# Thinking Guides - -> **Purpose**: Expand your thinking to catch things you might not have considered. - ---- - -## Why Thinking Guides? - -**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill: - -- Didn't think about what happens at layer boundaries → cross-layer bugs -- Didn't think about code patterns repeating → duplicated code everywhere -- Didn't think about edge cases → runtime errors -- Didn't think about future maintainers → unreadable code - -These guides help you **ask the right questions before coding**. - ---- - -## Available Guides - -| Guide | Purpose | When to Use | -|-------|---------|-------------| -| [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md) | Identify patterns and reduce duplication | When you notice repeated patterns | -| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Think through data flow across layers | Features spanning multiple layers | - ---- - -## Quick Reference: Thinking Triggers - -### When to Think About Cross-Layer Issues - -- [ ] Feature touches 3+ layers (API, Service, Component, Database) -- [ ] Data format changes between layers -- [ ] Multiple consumers need the same data -- [ ] You're not sure where to put some logic - -→ Read [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) - -### When to Think About Code Reuse - -- [ ] You're writing similar code to something that exists -- [ ] You see the same pattern repeated 3+ times -- [ ] You're adding a new field to multiple places -- [ ] **You're modifying any constant or config** -- [ ] **You're creating a new utility/helper function** ← Search first! - -→ Read [Code Reuse Thinking Guide](./code-reuse-thinking-guide.md) - ---- - -## Pre-Modification Rule (CRITICAL) - -> **Before changing ANY value, ALWAYS search first!** - -```bash -# Search for the value you're about to change -grep -r "value_to_change" . -``` - -This single habit prevents most "forgot to update X" bugs. - ---- - -## How to Use This Directory - -1. **Before coding**: Skim the relevant thinking guide -2. **During coding**: If something feels repetitive or complex, check the guides -3. **After bugs**: Add new insights to the relevant guide (learn from mistakes) - ---- - -## Contributing - -Found a new "didn't think of that" moment? Add it to the relevant guide. - ---- - -**Core Principle**: 30 minutes of thinking saves 3 hours of debugging. diff --git a/.trellis/spec/rtl/index.md b/.trellis/spec/rtl/index.md deleted file mode 100644 index 141e2fe..0000000 --- a/.trellis/spec/rtl/index.md +++ /dev/null @@ -1,12 +0,0 @@ -# RTL Specifications - -## Pre-Development Checklist - -Before writing RTL code or testbenches, read: -1. [XSIM Testbench Conventions](./xsim-tb-conventions.md) — for Vivado XSIM Verilog testbenches - -## Files - -| File | Purpose | -|------|---------| -| `xsim-tb-conventions.md` | Vivado XSIM Verilog testbench conventions (template, vector format, TCL scripts) | diff --git a/.trellis/spec/rtl/xsim-tb-conventions.md b/.trellis/spec/rtl/xsim-tb-conventions.md deleted file mode 100644 index e174fdc..0000000 --- a/.trellis/spec/rtl/xsim-tb-conventions.md +++ /dev/null @@ -1,209 +0,0 @@ -# Vivado XSIM Verilog Testbench Conventions - -## Purpose - -Conventions for writing Verilog testbenches targeting Vivado XSIM simulator. -These testbenches co-exist with Verilator C++ testbenches (`.cpp`) — both go in the same `TB/` directory. - -## Directory Structure - -``` -sync_rtl/<module>/ -├── <module>.v # RTL source -├── TB/ -│ ├── tb_<module>.cpp # Verilator C++ testbench (optional) -│ ├── tb_<module>_xsim.v # XSIM Verilog testbench -│ ├── gen_vectors.py # Python vector generator (stdlib only) -│ ├── xsim_run.tcl # Vivado compile+elaborate+simulate script -│ └── vectors/ -│ └── <module>_input.hex # Test input vectors for $readmemh -``` - -## TB File Template - -```verilog -// tb_<module>_xsim.v - Vivado XSIM testbench for <module> -// Usage: -// xvlog -sv <deps> <module>.v tb_<module>_xsim.v -// xelab tb_<module>_xsim -s <snapshot> -// xsim <snapshot> -R - -`timescale 1ns / 1ps - -module tb_<module>_xsim; - - parameter VECTOR_FILE = "sync_rtl/<module>/TB/vectors/<module>_input.hex"; - parameter TIMEOUT_CYCLES = 10000; - - // DUT signals (reg for inputs, wire for outputs) - reg clk, rst_n; - // ... module-specific ports ... - - // DUT instantiation - <module> u_dut ( - .clk(clk), .rst_n(rst_n), - // ... - ); - - // Clock: 100 MHz (10 ns period) - initial clk = 1'b0; - always #5 clk = ~clk; - - // Vector memory (width depends on module) - reg [W-1:0] vector_mem [0:MAX_VECTORS-1]; - - integer pass_count, fail_count; - - initial begin - // Load vectors - $readmemh(VECTOR_FILE, vector_mem); - - // Reset: rst_n low 3 cycles - rst_n <= 1'b0; - repeat (3) @(posedge clk); - rst_n <= 1'b1; - @(posedge clk); - - // Process each vector: drive → wait valid_o → capture → verify - for (idx = 0; idx < vec_count; idx = idx + 1) begin - // Drive DUT inputs (use <= for reg drives) - // ... - - // Wait for valid_o with timeout - cycle_count = 0; - while (!valid_o && cycle_count < TIMEOUT_CYCLES) begin - @(posedge clk); - cycle_count = cycle_count + 1; - end - - if (cycle_count >= TIMEOUT_CYCLES) begin - $display("ERROR: Timeout on vector %0d", idx); - fail_count = fail_count + 1; - end else begin - // Capture output, verify, write result - pass_count = pass_count + 1; - end - end - - $display("PASS: %0d FAIL: %0d", pass_count, fail_count); - $finish; - end - - // Timeout watchdog - initial begin - #(TIMEOUT_CYCLES * 10 * 100); - $display("FATAL: Global timeout"); - $finish; - end - -endmodule -``` - -## Key Rules - -### Clock -- Always: `initial clk = 1'b0; always #5 clk = ~clk;` (100 MHz, 10 ns period) - -### Reset -- Active-low (`rst_n`), held low for 3 cycles minimum -- Pattern: `rst_n <= 1'b0; repeat (3) @(posedge clk); rst_n <= 1'b1; @(posedge clk);` - -### DUT Drive Protocol -- Use **non-blocking assignment** (`<=`) for all DUT input drives in `initial` blocks -- Use **blocking assignment** (`=`) for local variable initialization only -- Follow valid/ready handshake: assert `valid_i`, wait `ready_o`, then de-assert - -### Timeout Watchdog -- Every TB MUST have a global timeout watchdog initial block -- Pattern: `#(TIMEOUT_CYCLES * 10 * 100);` (gives TIMEOUT_CYCLES × 1000ns margin) -- Module-specific per-vector timeouts also required (using `cycle_count < TIMEOUT_CYCLES` loop) - -### Pass/Fail Tracking -- Use `integer pass_count, fail_count;` -- Increment on each vector completion or timeout -- Print summary with `$display("PASS: %0d FAIL: %0d", pass_count, fail_count)` before `$finish` - -## Vector Format (`$readmemh`) - -Vectors are packed as single hex numbers per line: - -``` -// Simple module (e.g., mod_add, 24-bit vectors) -// Hex chars = ceil(W/4) -000000 -0640c8 -d00d00 -``` - -``` -// Complex module (e.g., ntt_core, 3076-bit vectors) -// Hex chars = 769 -00000000...0000 -10000000...0001 -``` - -Width `W` must evenly contain all packed fields. Use padding bits to align to hex-char boundaries (multiples of 4 bits). - -## Running Testbenches - -### Quick Run -```bash -# List available modules -./run_tb.sh --list - -# Run a specific module -./run_tb.sh mod_add -``` - -### Manual Run -```bash -source /opt/Xilinx/Vivado/2019.2/settings64.sh -export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # required for Vivado 2019.2 on modern Linux -cd ~/Dev/mlkem -vivado -mode batch -source sync_rtl/mod_add/TB/xsim_run.tcl -``` - -### Vivado 2019.2 Compatibility Notes -- **Include flag**: Use `-i .` (not `-include_dirs .` — that's Vivado 2020+) -- **ncurses fix**: `export LD_PRELOAD=/usr/lib64/libtinfo.so.5` resolves `_nc_tiparm` symbol error from bundled LLVM 3.1 -- **Timescale**: Always pass `--timescale 1ns/1ps` to `xelab` (RTL modules may lack `timescale directives) - -## xsim_run.tcl - -```tcl -# NOTE: On some systems, you may need: -# export LD_PRELOAD=/usr/lib64/libtinfo.so.5 - -# Compile RTL dependencies (order matters for submodule hierarchy) -xvlog -sv -i . <rtl_dir>/<submodule>.v -xvlog -sv -i . <rtl_dir>/<dut>.v - -# Compile testbench -xvlog -sv <tb_dir>/tb_<module>_xsim.v - -# Elaborate -xelab tb_<module>_xsim -s <snapshot> --timescale 1ns/1ps - -# Run -xsim <snapshot> -R -``` - -### CRITICAL: Include Directories -- If ANY compiled file uses `` `include "sync_rtl/common/defines.vh" `` (or any relative include), add `-i .` to ALL `xvlog` invocations in that TCL -- **Vivado 2019.2**: Use `-i .` (single dash) -- **Vivado 2020+**: Use `-include_dirs .` (single or double dash) - -## gen_vectors.py - -- **Stdlib only**: Use only Python standard library modules (`hashlib`, `os`, `sys`, `random`, `math`) -- **Output**: Write hex vectors to `vectors/<module>_input.hex`, one packed hex number per line -- **Self-contained**: Should be runnable standalone (`python3 gen_vectors.py`) -- **Bit ordering**: Match RTL FIPS 202 bit ordering — seed[0] = first bit into sponge. For Python hashlib, this means `bytes.fromhex(seed_hex)[::-1]` (reverse byte order) - -## Common Mistakes - -1. **Forgetting `-include_dirs .`**: DUTs using `` `include "sync_rtl/common/defines.vh" `` will fail compilation if the project root is not in the include path -2. **Using `=` for DUT drives**: All DUT input assignments in `initial` blocks must use `<=`, not `=`, to avoid race conditions -3. **Missing timeout watchdog**: If the DUT deadlocks, the simulation will hang forever without a watchdog -4. **Bit order mismatch**: Python hashlib and Verilog RTL may have different bit ordering for SHA3/SHAKE operations — verify with known test vectors -5. **rush_n polarity**: The reset is active-low (`rst_n`), not active-high — held to 0 for reset, 1 for normal operation diff --git a/.trellis/tasks/00-bootstrap-guidelines/prd.md b/.trellis/tasks/00-bootstrap-guidelines/prd.md deleted file mode 100644 index dbad710..0000000 --- a/.trellis/tasks/00-bootstrap-guidelines/prd.md +++ /dev/null @@ -1,139 +0,0 @@ -# Bootstrap Task: Fill Project Development Guidelines - -**You (the AI) are running this task. The developer does not read this file.** - -The developer just ran `trellis init` on this project for the first time. -`.trellis/` now exists with empty spec scaffolding, and this bootstrap task -exists under `.trellis/tasks/`. When they want to work on it, they should start -this task from a session that provides Trellis session identity. - -**Your job**: help them populate `.trellis/spec/` with the team's real -coding conventions. Every future AI session — this project's -`trellis-implement` and `trellis-check` sub-agents — auto-loads spec files -listed in per-task jsonl manifests. Empty spec = sub-agents write generic -code. Real spec = sub-agents match the team's actual patterns. - -Don't dump instructions. Open with a short greeting, figure out if the repo -has any existing convention docs (CLAUDE.md, .cursorrules, etc.), and drive -the rest conversationally. - ---- - -## Status (update the checkboxes as you complete each item) - -- [ ] Fill backend guidelines -- [ ] Fill frontend guidelines -- [ ] Add code examples - ---- - -## Spec files to populate - - -### Backend guidelines - -| File | What to document | -|------|------------------| -| `.trellis/spec/backend/directory-structure.md` | Where different file types go (routes, services, utils) | -| `.trellis/spec/backend/database-guidelines.md` | ORM, migrations, query patterns, naming conventions | -| `.trellis/spec/backend/error-handling.md` | How errors are caught, logged, and returned | -| `.trellis/spec/backend/logging-guidelines.md` | Log levels, format, what to log | -| `.trellis/spec/backend/quality-guidelines.md` | Code review standards, testing requirements | - - -### Frontend guidelines - -| File | What to document | -|------|------------------| -| `.trellis/spec/frontend/directory-structure.md` | Component/page/hook organization | -| `.trellis/spec/frontend/component-guidelines.md` | Component patterns, props conventions | -| `.trellis/spec/frontend/hook-guidelines.md` | Custom hook naming, patterns | -| `.trellis/spec/frontend/state-management.md` | State library, patterns, what goes where | -| `.trellis/spec/frontend/type-safety.md` | TypeScript conventions, type organization | -| `.trellis/spec/frontend/quality-guidelines.md` | Linting, testing, accessibility | - - -### Thinking guides (already populated) - -`.trellis/spec/guides/` contains general thinking guides pre-filled with -best practices. Customize only if something clearly doesn't fit this project. - ---- - -## How to fill the spec - -### Step 1: Import from existing convention files first (preferred) - -Search the repo for existing convention docs. If any exist, read them and -extract the relevant rules into the matching `.trellis/spec/` files — -usually much faster than documenting from scratch. - -| File / Directory | Tool | -|------|------| -| `CLAUDE.md` / `CLAUDE.local.md` | Claude Code | -| `AGENTS.md` | Codex / Claude Code / agent-compatible tools | -| `.cursorrules` | Cursor | -| `.cursor/rules/*.mdc` | Cursor (rules directory) | -| `.windsurfrules` | Windsurf | -| `.clinerules` | Cline | -| `.roomodes` | Roo Code | -| `.github/copilot-instructions.md` | GitHub Copilot | -| `.vscode/settings.json` → `github.copilot.chat.codeGeneration.instructions` | VS Code Copilot | -| `CONVENTIONS.md` / `.aider.conf.yml` | aider | -| `CONTRIBUTING.md` | General project conventions | -| `.editorconfig` | Editor formatting rules | - -### Step 2: Analyze the codebase for anything not covered by existing docs - -Scan real code to discover patterns. Before writing each spec file: -- Find 2-3 real examples of each pattern in the codebase. -- Reference real file paths (not hypothetical ones). -- Document anti-patterns the team clearly avoids. - -### Step 3: Document reality, not ideals - -**Critical**: write what the code *actually does*, not what it should do. -Sub-agents match the spec, so aspirational patterns that don't exist in the -codebase will cause sub-agents to write code that looks out of place. - -If the team has known tech debt, document the current state — improvement -is a separate conversation, not a bootstrap concern. - ---- - -## Quick explainer of the runtime (share when they ask "why do we need spec at all") - -- Every AI coding task spawns two sub-agents: `trellis-implement` (writes - code) and `trellis-check` (verifies quality). -- Each task has `implement.jsonl` / `check.jsonl` manifests listing which - spec files to load. -- The platform hook auto-injects those spec files + the task's `prd.md` - into every sub-agent prompt, so the sub-agent codes/reviews per team - conventions without anyone pasting them manually. -- Source of truth: `.trellis/spec/`. That's why filling it well now pays - off forever. - ---- - -## Completion - -When the developer confirms the checklist items above are done with real -examples (not placeholders), guide them to run: - -```bash -python3 ./.trellis/scripts/task.py finish -python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines -``` - -After archive, every new developer who joins this project will get a -`00-join-<slug>` onboarding task instead of this bootstrap task. - ---- - -## Suggested opening line - -"Welcome to Trellis! Your init just set me up to help you fill the project -spec — a one-time setup so every future AI session follows the team's -conventions instead of writing generic code. Before we start, do you have -any existing convention docs (CLAUDE.md, .cursorrules, CONTRIBUTING.md, -etc.) I can pull from, or should I scan the codebase from scratch?" diff --git a/.trellis/tasks/00-bootstrap-guidelines/task.json b/.trellis/tasks/00-bootstrap-guidelines/task.json deleted file mode 100644 index 46eb5c1..0000000 --- a/.trellis/tasks/00-bootstrap-guidelines/task.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "00-bootstrap-guidelines", - "name": "00-bootstrap-guidelines", - "title": "Bootstrap Guidelines", - "description": "Fill in project development guidelines for AI agents", - "status": "in_progress", - "dev_type": "docs", - "scope": null, - "package": null, - "priority": "P1", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-24", - "completedAt": null, - "branch": null, - "base_branch": null, - "worktree_path": null, - "commit": null, - "pr_url": null, - "subtasks": [], - "children": [], - "parent": null, - "relatedFiles": [ - ".trellis/spec/backend/", - ".trellis/spec/frontend/" - ], - "notes": "First-time setup task created by trellis init (fullstack project)", - "meta": {} -} \ No newline at end of file diff --git a/.trellis/tasks/06-27-sha3-g-test-specific-input/check.jsonl b/.trellis/tasks/06-27-sha3-g-test-specific-input/check.jsonl deleted file mode 100644 index 8f97ed3..0000000 --- a/.trellis/tasks/06-27-sha3-g-test-specific-input/check.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "检查 testbench 修改是否符合 XSIM 规范"} diff --git a/.trellis/tasks/06-27-sha3-g-test-specific-input/implement.jsonl b/.trellis/tasks/06-27-sha3-g-test-specific-input/implement.jsonl deleted file mode 100644 index 6696aa7..0000000 --- a/.trellis/tasks/06-27-sha3-g-test-specific-input/implement.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "遵循 XSIM testbench 编写规范:时钟、复位、DUT驱动协议"} diff --git a/.trellis/tasks/06-27-sha3-g-test-specific-input/prd.md b/.trellis/tasks/06-27-sha3-g-test-specific-input/prd.md deleted file mode 100644 index d7118a3..0000000 --- a/.trellis/tasks/06-27-sha3-g-test-specific-input/prd.md +++ /dev/null @@ -1,30 +0,0 @@ -# 修改 sha3 TB 测试 G 模式指定输入 - -## 需求 - -修改 `sync_rtl/sha3/TB/tb_sha3_xsim_simple.v`,将其中硬编码的测试向量替换为指定的 d、k 值,测试 sha3_top 的 G (SHA3-512) 模式。 - -## 测试输入 - -``` -d = 0x6dbbc4375136df3b07f7c70e639e223e177e7fd53b161b3f4d57791794f12624 (256-bit) -k = 2 (8-bit) -mode = G (2'b00) -``` - -在 RTL 中,G 模式的输入格式为 `data_i[263:0] = {d[255:0], k[7:0]}`(d 在低位,k 在高位),即: - -``` -data_i = {248'd0, 8'd2, 256'h6dbbc4375136df3b07f7c70e639e223e177e7fd53b161b3f4d57791794f12624} -``` - -## 修改方案 - -修改 `tb_sha3_xsim_simple.v`: -1. 将 `data_i = 512'd0` 替换为指定值 `{248'd0, 8'd2, 256'h6dbbc4375136df3b07f7c70e639e223e177e7fd53b161b3f4d57791794f12624}` -2. 删除 `G_EXPECTED_HASH` 参数和自检比较逻辑,改为仅打印 `hash_o` 结果 -3. 更新顶部的注释说明 - -## 预期输出 - -运行后打印 `hash_o` 的值,可通过 Python 参考实现 `SHA_3.G(d, k)` 交叉验证。 diff --git a/.trellis/tasks/06-27-sha3-g-test-specific-input/task.json b/.trellis/tasks/06-27-sha3-g-test-specific-input/task.json deleted file mode 100644 index ccd10c5..0000000 --- a/.trellis/tasks/06-27-sha3-g-test-specific-input/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "sha3-g-test-specific-input", - "name": "sha3-g-test-specific-input", - "title": "修改sha3 TB测试G模式指定输入d=k=2", - "description": "", - "status": "in_progress", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "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/tasks/archive/2026-06/06-25-fix-tb-failures/check.jsonl b/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/check.jsonl deleted file mode 100644 index 9d17183..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/check.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB quality checks — TCL format, timing, part-select constraints"} diff --git a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/implement.jsonl b/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/implement.jsonl deleted file mode 100644 index 23013e3..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/implement.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB conventions — TCL format, part-select constraints, timing patterns"} diff --git a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/prd.md b/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/prd.md deleted file mode 100644 index faa4d3c..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/prd.md +++ /dev/null @@ -1,39 +0,0 @@ -# 修复 7 个失败的 testbench - -## Goal - -修复在 Vivado 2019.2 上运行失败的 7 个 testbench:comp_decomp, storage, sha3_chain, ntt, poly_mul, sample_cbd, sample_ntt。 - -## Root Causes - -### 1. TCL 变量缺失 (sha3_chain, sample_cbd, sample_ntt) -`run_tb.sh` 只提取 `set SRC_DIR/TB_DIR/COMMON_DIR`,但这些 TCL 还定义了 `SHA3_DIR` 等变量用于引用 sha3 依赖。 - -### 2. Verilog part-select 非常量 (ntt, poly_mul) -```verilog -t_coeffs[ci] = vector_mem[idx][(255-ci)*12+11 : (255-ci)*12]; -``` -Vivado 2019.2 要求 part-select 的边界必须是常量表达式,`ci` 是变量导致编译失败。需要改用 `+:` 或 `for` 循环 + `case`。 - -### 3. BRAM 读延迟未对齐 (storage) -s_bram/sd_bram 有 1 周期读延迟:assert rd_addr 后,下一周期 rd_data 才有效。TB 在同一周期检查 rd_data。 - -### 4. comp_decomp 位宽边界 (comp_decomp) -d=12 时 compress 输出需要 12 位(2^12=4096),期望值 4095 正确但 DUT 的 `coeff_out` 只有 12 位。实际问题是 compress mod 2^d 在边界处产生 0(因为 4095+1 进位溢出)。需要检查 gen_vectors.py 的期望值生成逻辑。 - -## Requirements - -* sha3_chain, sample_cbd, sample_ntt: 修复 xsim_run.tcl 中缺失的变量定义 -* ntt, poly_mul: 修复 part-select 为非变量形式 -* storage: 修复读延迟(插入 1 个时钟周期等待) -* comp_decomp: 修复期望值或 TB 比较逻辑 -* 所有 10 个 TB 在 Vivado 2019.2 上 PASS - -## Decisions Made - -* 直接修复现有 TB 文件,不重新生成 -* 遵循 Vivado 2019.2 SystemVerilog 约束 - -## Out of Scope - -* RTL 修改 diff --git a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/task.json b/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/task.json deleted file mode 100644 index e3a1167..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-fix-tb-failures/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "fix-tb-failures", - "name": "fix-tb-failures", - "title": "修复7个失败的testbench", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-25", - "completedAt": "2026-06-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-06/06-25-vivado-verilog-tb/check.jsonl b/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/check.jsonl deleted file mode 100644 index d084625..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/check.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/verilator-conventions.md", "reason": "RTL conventions for quality checking: clock period, valid/ready timing, testbench structure"} diff --git a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/implement.jsonl b/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/implement.jsonl deleted file mode 100644 index 6393e98..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/implement.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"file": ".trellis/spec/rtl/verilator-conventions.md", "reason": "RTL conventions: clock period 10ns, valid/ready timing protocol, pipeline_reg behavior"} diff --git a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/prd.md b/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/prd.md deleted file mode 100644 index 26c2357..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/prd.md +++ /dev/null @@ -1,167 +0,0 @@ -# 编写所有模块的 Vivado Verilog Testbench - -## Goal - -为 ML-KEM 硬件项目的所有 RTL 模块编写 Verilog testbench(.v 文件),可在 Vivado xsim 中仿真。遵循已有 sha3/TB/ 的 xsim testbench 约定和风格。 - -## What I already know - -### 项目结构 -- 项目根目录:`/home/fallensigh/Dev/mlkem` -- 所有 RTL 源码在 `sync_rtl/` 下按功能分目录 -- 已有 testbench 在各自的 `TB/` 子目录下 - -### 已有 testbench 情况 -| 模块目录 | 已有 Verilator (.cpp) TB | 已有 XSIM (.v) TB | 需要新增 .v TB | -|----------|------------------------|-------------------|---------------| -| sha3/ | ✅ tb_sha3.cpp | ✅ tb_sha3_xsim.v, tb_sha3_xsim_simple.v, tb_keccak_core_xsim.v | ❌ 已完成 | -| sha3_chain/ | ✅ tb_sha3_chain.cpp | ❌ | ✅ | -| ntt/ | ✅ tb_ntt.cpp | ❌ | ✅ | -| poly_mul/ | ✅ tb_poly_mul.cpp | ❌ | ✅ | -| poly_arith/ | ✅ tb_poly_arith.cpp | ❌ | ✅ | -| sample_cbd/ | ✅ tb_sample_cbd.cpp | ❌ | ✅ | -| sample_ntt/ | ✅ tb_sample_ntt.cpp | ❌ | ✅ | -| rng/ | ✅ tb_rng.cpp | ❌ | ✅ | -| comp_decomp/ | ✅ tb_comp_decomp.cpp | ❌ | ✅ | -| storage/ | ✅ tb_storage.cpp | ❌ | ✅ | -| mod_add/ | ✅ tb_mod_add.cpp | ❌ | ✅ | -| common/ | ❌ | ❌ | ✅ (pipeline_reg, skid_buffer) | - -### RTL 模块清单(按目录) - -**sha3/** (已有 TB): -- `sha3_top.v` - SHA3 顶层(G/H/J 模式) -- `keccak_core.v` - Keccak 24 轮核心 -- `keccak_round.v` - 单轮 Keccak(组合逻辑) - -**sha3_chain/**: -- `sha3_chain_top.v` - SHA3 链式调用顶层 - -**ntt/**: -- `ntt_sync.v` - NTT 同步顶层(wrapper) -- `ntt_core.v` - NTT 核心(Barrett 约简 + butterfly) -- `butterfly_unit.v` - 蝶形运算单元 -- `barrett_mul.v` - Barrett 模乘 -- `zeta_rom.v` - Zeta 预计算 ROM - -**poly_mul/**: -- `poly_mul_sync.v` - 多项式乘法顶层 -- `poly_mul_zeta_rom.v` - PolyMul zeta ROM -- `basecase_mul.v` - 基础乘法 - -**poly_arith/**: -- `poly_arith_sync.v` - 多项式加减(PolyAdd/PolySub) - -**sample_cbd/**: -- `sample_cbd_sync.v` - CBD 采样(SHAKE-256 PRF) - -**sample_ntt/**: -- `sample_ntt_sync.v` - NTT 域采样 - -**rng/**: -- `rng_sync.v` - Galois LFSR PRNG(256-bit) - -**comp_decomp/**: -- `comp_decomp_sync.v` - 压缩/解压缩 - -**storage/**: -- `s_bram.v` - 单口 BRAM -- `sd_bram.v` - 简单双口 BRAM - -**mod_add/**: -- `mod_add_sync.v` - 模加法 - -**common/**: -- `pipeline_reg.v` - 流水线寄存器 -- `skid_buffer.v` - Skid Buffer - -### 现有 XSIM TB 约定(参考 sha3/TB/) -- 命名:`tb_<module>_xsim.v` -- 时钟:100MHz,10ns 周期(`always #5 clk = ~clk`) -- 复位:`rst_n` 低有效 3 周期 -- 自检模式:hardcode 期望值,用 `$error` 报告 mismatch -- 文件向量模式:`$readmemh` 从 hex 文件读测试向量 -- 输出:`$display` 日志 + `$fwrite` 结果文件 -- 超时看门狗:防止死锁 -- 编译脚本:`xsim_run.tcl` (xvlog → xelab → xsim) - -### 接口约定(从各模块 port 列表得出) -- 所有模块使用 synchronous valid/ready 握手 -- `clk, rst_n` 标准端口 -- 数据宽度:12-bit 系数(ntt/poly),256-bit(rng),512-bit(sha3) -- 部分模块有 `done_o` 信号(如 ntt_sync) - -## Assumptions - -1. 测试自检模式为主,可对比 Python 参考实现的期望输出 -2. 简单模块(common/)用 hardcoded 向量测试 -3. 复杂模块可能需要 hex 文件向量 -4. 子模块(butterfly_unit, barrett_mul, zeta_rom, basecase_mul 等)通过父模块间接测试 - -## Decisions Made - -* **测试范围**:只测顶层 sync 模块,子模块通过父模块间接覆盖 -* **common/** 模块(pipeline_reg, skid_buffer)不在本次范围内 -* **测试模式**:文件向量模式(`$readmemh` 读 hex 文件),参考 `tb_sha3_xsim.v` 风格 -* **向量生成**:每个模块配套 Python `gen_vectors.py` 生成输入向量 `.hex` 文件 -* **编译脚本**:每个模块目录一个独立的 `xsim_run.tcl` - -## 需要编写 TB 的模块清单(10 个) - -| # | 模块 | 文件 | 接口要点 | -|---|------|------|---------| -| 1 | `sha3_chain_top` | `sync_rtl/sha3_chain/sha3_chain_top.v` | d_in[255:0], start_i → rho_out[255:0], sigma_out[255:0] | -| 2 | `ntt_core` | `sync_rtl/ntt/ntt_core.v` | coeff_in[11:0]×256, mode → coeff_out[11:0]×256, done_o | -| 3 | `poly_mul_sync` | `sync_rtl/poly_mul/poly_mul_sync.v` | coeff_a/b ×256 → coeff_out ×256 | -| 4 | `poly_arith_sync` | `sync_rtl/poly_arith/poly_arith_sync.v` | coeff_a/b[11:0], mode → coeff_out[11:0] (流式) | -| 5 | `sample_cbd_sync` | `sync_rtl/sample_cbd/sample_cbd_sync.v` | seed_i[255:0], nonce_i[7:0], eta_i → coeff_o[11:0]×256 | -| 6 | `sample_ntt_sync` | `sync_rtl/sample_ntt/sample_ntt_sync.v` | rho_i[255:0], k/i/j_idx → coeff_o[11:0]×256 | -| 7 | `rng_sync` | `sync_rtl/rng/rng_sync.v` | valid_i → data_o[255:0] (LFSR) | -| 8 | `comp_decomp_sync` | `sync_rtl/comp_decomp/comp_decomp_sync.v` | coeff_in[11:0], d[4:0], mode → coeff_out[11:0] | -| 9 | `s_bram` / `sd_bram` | `sync_rtl/storage/s_bram.v`, `sd_bram.v` | 参数化 BRAM 读写 | -| 10 | `mod_add_sync` | `sync_rtl/mod_add/mod_add_sync.v` | a[11:0], b[11:0] → sum[11:0] | - -## Requirements (final) - -* 10 个顶层 sync 模块各产出 4 个文件: - - `TB/tb_<module>_xsim.v` — Verilog testbench(文件向量模式,`$readmemh` + `$fwrite`) - - `TB/vectors/<module>_input.hex` — 测试输入向量 - - `TB/gen_vectors.py` — Python 脚本生成输入向量 - - `TB/xsim_run.tcl` — Vivado 编译+仿真脚本 -* 遵循 sha3/TB/ 的编码风格和目录结构: - - 时钟 100MHz (`always #5 clk = ~clk`) - - 复位 `rst_n` 低有效 3 周期 - - valid/ready 握手协议 - - 超时看门狗 - - pass/fail 统计 + `$display` 报告 -* 支持 `xvlog -sv` + `xelab` + `xsim -R` 流程 - -## Acceptance Criteria - -* [ ] 10 个模块各有完整的 TB 目录:tb .v + gen_vectors.py + xsim_run.tcl -* [ ] 每个 testbench 可通过 `xvlog -sv` 编译无 error -* [ ] 每个 testbench 可通过 `xelab` + `xsim -R` 跑通仿真 -* [ ] 输出清晰的 pass/fail 报告 -* [ ] 包含超时看门狗防止死锁 -* [ ] Python `gen_vectors.py` 可独立运行生成 hex 向量文件 - -## Definition of Done - -* 所有 10 个模块的 4 类文件已创建 -* 每个 TB 遵循 `sha3/TB/` 的代码风格 -* 向量文件格式与 `$readmemh` 兼容 - -## Out of Scope - -* Verilator C++ testbench(已有) -* 覆盖率收集 -* UVM 框架 -* 子模块独立 TB(butterfly_unit, barrett_mul, basecase_mul, ROM 等) -* common/ 模块 TB(pipeline_reg, skid_buffer) - -## Technical Notes - -- 参考已有 TB:`sync_rtl/sha3/TB/tb_sha3_xsim.v`(文件向量模式)、`sync_rtl/sha3/TB/tb_sha3_xsim_simple.v`(简单自检模式) -- 编译脚本参考:`sync_rtl/sha3/TB/xsim_run.tcl` -- RTL spec:`.trellis/spec/rtl/verilator-conventions.md`(Verilator 相关,部分适用) -- 时钟定义:`sync_rtl/common/defines.vh` 中的 `CLK_PERIOD 10.0` diff --git a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/task.json b/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/task.json deleted file mode 100644 index 03c2dbe..0000000 --- a/.trellis/tasks/archive/2026-06/06-25-vivado-verilog-tb/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "vivado-verilog-tb", - "name": "vivado-verilog-tb", - "title": "编写所有模块的Vivado Verilog Testbench", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-25", - "completedAt": "2026-06-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-06/06-26-mlkem-top-integration/check.jsonl b/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/check.jsonl deleted file mode 100644 index 3ae1cec..0000000 --- a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/check.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB quality checks: TCL format, testbench structure, pass/fail"} - diff --git a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/implement.jsonl b/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/implement.jsonl deleted file mode 100644 index d2cf8d8..0000000 --- a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/implement.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"file": ".trellis/spec/rtl/verilator-conventions.md", "reason": "RTL conventions: clock 10ns, valid/ready timing, pipeline_reg behavior"} -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB conventions: TCL format, part-select, timing"} - diff --git a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/prd.md b/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/prd.md deleted file mode 100644 index 849f353..0000000 --- a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/prd.md +++ /dev/null @@ -1,99 +0,0 @@ -# ML-KEM 顶层集成模块 - -## Goal - -实现 `mlkem_top` 顶层模块,将现有 10 个子模块集成为完整的 ML-KEM 系统,支持 KeyGen、Encaps、Decaps 三个操作。对外使用 valid/ready 流式接口。 - -## Decisions Made - -* **操作范围**:KeyGen + Encaps + Decaps 全部实现 -* **接口风格**:valid/ready 流式接口(类似 AXI-Stream) -* **k 值支持**:全支持 k=2,3,4,通过 `parameter K=4` 参数化,通过 `i_k[2:0]` 输入选择 -* **Keccak 策略**:共享 1 个 `keccak_core` 实例,各消费者通过仲裁器复用。需重构 `sha3_chain_top`/`sample_cbd_sync`/`sample_ntt_sync` 接受外部 Keccak 接口 - -## What I already know - -### 现有模块接口汇总 - -| 模块 | 协议 | 关键信号 | 数据宽度 | 延迟 | -|------|------|---------|:---:|------| -| `rng_sync` | v/r | `valid_i`→`data_o[255]` | 256 | 1 cycle/word | -| `sha3_chain_top` | start/done | `start_i`→`done_o`→`rho_out/sigma_out[255]` | 256→512 | ~24 cycles | -| `sha3_top` | v/r | `mode[1:0]`, `data_i[512]`→`hash_o[512]` | 512→512 | ~24 cycles | -| `sample_cbd_sync` | v/r+last | `seed_i[255],nonce_i[8],eta_i[2]`→`coeff_o[12]×256` | — | ~300 cycles | -| `sample_ntt_sync` | v/r+last | `rho_i[255],k_i,i_idx,j_idx`→`coeff_o[12]×256` | — | ~4000 cycles | -| `ntt_core` | v/r+done | `coeff_in[12]×256,mode`→`coeff_out[12]×256` | 12→12 | ~200 cycles | -| `poly_arith_sync` | v/r | `coeff_a/b[12],mode`→`coeff_out[12]` | 12→12 | 1 cycle/coeff | -| `poly_mul_sync` | v/r | `coeff_a/b[12]×256`→`coeff_out[12]×256` | 12→12 | ~300 cycles | -| `mod_add_sync` | v/r | `a,b[12]`→`sum[12]` | 12→12 | 1 cycle | -| `comp_decomp_sync` | v/r | `coeff_in[12],d[5],mode`→`coeff_out[12]` | 12→12 | 1 cycle/coeff | -| `s_bram` | raw | `rd_en,wr_en,addr,data` | 48 | 1 cycle | -| `sd_bram` | raw | `wr_en,addr,data` | 48 | 1 cycle | - -### ML-KEM 算法数据流 - -**KeyGen**: -1. `d = RNG(256)` → `sha3_chain(d)` → ρ, σ -2. for i in 0..k-1: `sample_ntt(ρ, k, i, 0)` → `ntt_core(NTT)` → Â[i] -3. for i in 0..k-1: `sample_cbd(σ, N=i)` → `ntt_core(NTT)` → ŝ[i] (秘密) -4. ŝ̂ = ŝ (already in NTT domain) -5. for i in 0..k-1: `poly_add(ê[i], ŝ̂[i])` → 再 `poly_mul(ê[i], Â[i])` -6. `t̂ = ê + ŝ̂`, then `comp_decomp(t̂)` → public key pk -7. `comp_decomp(ŝ̂)` → secret key sk - -**Encaps**: -1. `m = RNG(256)` -2. `m = SHA3-256(m)` → hash -3. `(K̄, r) = G(m || H(pk))` → SHA3-512 -4. for i: `sample_ntt(ρ, k, i, 0)` → `ntt_core(NTT)` → Â[i] -5. for i: `sample_cbd(r, N=i)` → `ntt_core(NTT)` → ŷ[i] -6. `u = NTT^{-1}(Â^T · ŷ) + sample_cbd(r, N=k+i)` -7. `v = NTT^{-1}(t̂^T · ŷ) + sample_cbd(r, N=2k) + decompress(m)` -8. `c1 = compress(u)`, `c2 = compress(v)` -9. `K = KDF(K̄ || SHA3-256(c))` - -**Decaps**: -1. `u = decompress(c1)`, `v = decompress(c2)` -2. `u` → `ntt_core(NTT)` → û -3. `v - NTT^{-1}(ŝ̂^T · û)` → `comp_decomp(decompress)` → m' -4. `(K̄', r') = G(m' || H(pk))` -5. Re-encrypt with r' to get c' -6. If c == c', K = KDF(K̄' || SHA3-256(c)), else K = KDF(z || SHA3-256(c)) - -### 关键架构问题 - -1. **多项式存储**:中间多项式(Â[0..k-1], ŝ[0..k-1], ŷ[0..k-1] 等)需要 BRAM 存储 -2. **Keccak 共享**:sha3_chain_top 和 sample_ntt/sample_cbd 内部都有 sha3_top 实例,需决定共享还是各自独立 -3. **RNG 重用**:KeyGen/Encaps 中多次调用 RNG,需确定是串行复用还是并行 - -## Open Questions - -(全部已解决) - -## Requirements (final) - -* 三合一顶层模块 `mlkem_top`,通过 mode[1:0] 选择操作 -* 对外 valid/ready 接口 -* 多项式中间结果存入 BRAM -* 单时钟域 100MHz -* RNG 复用(串行调用) - -## Acceptance Criteria (evolving) - -* [ ] KeyGen 产生正确的 (pk, sk) 输出 -* [ ] Encaps 产生正确的 (c, K) 输出 -* [ ] Decaps 产生正确的 K 输出 -* [ ] 验证 Encaps(Decaps()) consistency -* [ ] Vivado XSIM 可编译仿真 - -## Out of Scope - -* AXI4-Full 总线桥接 -* DMA 控制器 -* 性能优化(多模块并行) - -## Technical Notes - -- 参考 FIPS 203 Section 7 (Algorithm 15-17) -- 现有模块接口见上方表格 -- BRAM 参数:建议 W=12, D=256 (或 W=48 打包 4 系数) diff --git a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/task.json b/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/task.json deleted file mode 100644 index 64593de..0000000 --- a/.trellis/tasks/archive/2026-06/06-26-mlkem-top-integration/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "mlkem-top-integration", - "name": "mlkem-top-integration", - "title": "实现ML-KEM顶层集成模块(KeyGen/Encaps/Decaps)", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-26", - "completedAt": "2026-06-26", - "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-06/06-27-fix-kg-compute/check.jsonl b/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/check.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/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-06/06-27-fix-kg-compute/implement.jsonl b/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/implement.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/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-06/06-27-fix-kg-compute/prd.md b/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/prd.md deleted file mode 100644 index 1ea62f5..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/prd.md +++ /dev/null @@ -1,20 +0,0 @@ -# 修复 KeyGen 计算路径 - -## Goal -修复 `mlkem_top.v` 中 KeyGen 的占位状态,实现 t_hat、pk、sk 的实际计算,使 `./run_tb.sh top` 中 KeyGen 产生非零输出。 - -## Current Issues -1. KeyGen 的 t_hat 计算(poly_mul + poly_arith)是占位:`phase_done <= 1'b1` -2. `pk_o_r` / `sk_o_r` 从未被赋值(默认 0) -3. `rng_valid_i` 竞态(可能已跨过,仿真确认 FSM 在跑) - -## Fix Plan -1. 实现 `S_KG_TMUL_LOAD/COMP/OUT` 状态 → 调用 poly_mul_sync 计算 t_hat -2. 实现 `S_KG_ADD` → 调用 poly_arith_sync -3. 在 `S_KG_DONE` 中赋值 `pk_o_r = {t_hat polys, rho}` 和 `sk_o_r = {s_hat polys}` -4. 验证:`./run_tb.sh top` KeyGen 输出非零 - -## Out of Scope -- Encaps/Decaps 修复 -- pk/sk ByteEncode 编码(先输出 raw 12-bit coeffs) -- KAT 数值完全匹配 diff --git a/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/task.json b/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/task.json deleted file mode 100644 index 3ae5c89..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-kg-compute/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "fix-kg-compute", - "name": "fix-kg-compute", - "title": "修复KeyGen计算路径并输出正确的pk/sk", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "completedAt": "2026-06-27", - "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-06/06-27-fix-tb-strict-compare/check.jsonl b/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/check.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/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-06/06-27-fix-tb-strict-compare/implement.jsonl b/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/implement.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/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-06/06-27-fix-tb-strict-compare/prd.md b/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/prd.md deleted file mode 100644 index 68f109b..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/prd.md +++ /dev/null @@ -1,10 +0,0 @@ -# TB 严格数值比较 - -## Goal -修改 tb 中 PASS/FAIL 逻辑:仅当 pk/sk/ct/ss 与 KAT 期望值**数值匹配**才算 PASS,FSM 跑通但数值不匹配算 FAIL。 - -## Fix -在 `sync_rtl/top/TB/tb_mlkem_top_xsim.v` 中: -- 删除 "structural pass" 逻辑(line 320 附近) -- WARN 分支改为 `kg_fail++` 而非 `kg_pass++` -- Encaps/Decaps 同理 diff --git a/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/task.json b/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/task.json deleted file mode 100644 index 8932790..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-fix-tb-strict-compare/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "fix-tb-strict-compare", - "name": "fix-tb-strict-compare", - "title": "修改TB为严格数值比较", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "completedAt": "2026-06-27", - "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-06/06-27-kg-en-de-separate-tb/check.jsonl b/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/check.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/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-06/06-27-kg-en-de-separate-tb/implement.jsonl b/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/implement.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/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-06/06-27-kg-en-de-separate-tb/prd.md b/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/prd.md deleted file mode 100644 index 3b77426..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/prd.md +++ /dev/null @@ -1,18 +0,0 @@ -# KG/EN/DE 独立测试脚本 - -## Goal -编写3个独立测试:`./run_tb.sh kg`、`./run_tb.sh en`、`./run_tb.sh de`,各自只测试一个操作。 - -## Files to create - -| 文件 | 说明 | -|------|------| -| `sync_rtl/top/TB/tb_kg_xsim.v` | KeyGen only testbench | -| `sync_rtl/top/TB/tb_en_xsim.v` | Encaps only testbench | -| `sync_rtl/top/TB/tb_de_xsim.v` | Decaps only testbench | -| `sync_rtl/top/TB/xsim_run_kg.tcl` | KeyGen compile script | -| `sync_rtl/top/TB/xsim_run_en.tcl` | Encaps compile script | -| `sync_rtl/top/TB/xsim_run_de.tcl` | Decaps compile script | - -## Design -共享向量文件,各自只读取对应字段做单一操作测试。从 mlkem_top TB 派生,精简到只测一个操作。 diff --git a/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/task.json b/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/task.json deleted file mode 100644 index 96f04c1..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-kg-en-de-separate-tb/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "kg-en-de-separate-tb", - "name": "kg-en-de-separate-tb", - "title": "编写KG/EN/DE独立测试脚本", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "completedAt": "2026-06-27", - "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-06/06-27-mlkem-top-tb/check.jsonl b/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/check.jsonl deleted file mode 100644 index 663497a..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/check.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB quality checks"} - diff --git a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/implement.jsonl b/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/implement.jsonl deleted file mode 100644 index 84e0b09..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/implement.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"file": ".trellis/spec/rtl/xsim-tb-conventions.md", "reason": "XSIM TB conventions"} - diff --git a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/prd.md b/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/prd.md deleted file mode 100644 index c3803ad..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/prd.md +++ /dev/null @@ -1,50 +0,0 @@ -# mlkem_top KAT Testbench - -## Goal - -编写 `mlkem_top` 的 KAT (Known Answer Test) testbench,使用 FIPS 203 标准测试向量验证 KeyGen/Encaps/Decaps 正确性。 - -## Data Source - -`/home/fallensigh/Dev/ml-kem-r/test_data/kat_MLKEM_512.rsp` -- 100 个测试向量 (count 0-99) -- 每个向量包含:d, z, msg, pk, sk, ct, ss -- NIST FIPS 203 标准格式 - -## Requirements - -1. Python `gen_vectors.py` 解析 KAT 文件,生成 `mlkem_top_input.hex` 和 `mlkem_top_expected.hex` -2. Verilog `tb_mlkem_top_xsim.v`: - - 读取输入向量 (d, z, msg) - - 驱动 KeyGen → 验证 pk, sk - - 驱动 Encaps → 验证 ct, ss - - 驱动 Decaps → 验证 ss - - 超时看门狗 - - pass/fail 统计 - -## Decisions - -* 先用 ML-KEM-512 (k=2),后续扩展 768/1024 -* 文件向量模式,遵循现有 TB 风格 -* 自检模式:与 KAT 期望值逐个字节比较 - -## Test Flow (per vector) - -1. 复位 -2. KeyGen: d → verify pk, sk -3. Encaps: msg → verify ct, ss -4. Decaps: ct → verify ss (output matches ss from encaps) - -## Acceptance Criteria - -* [ ] 至少 count=0 的 KeyGen 通过 -* [ ] 至少 count=0 的 Encaps 通过 -* [ ] at least basic Decaps consistency -* [ ] xvlog 编译通过 -* [ ] xsim 仿真通过 - -## Out of Scope - -* 全部 100 个向量(先做前 5 个) -* ct_n 无效密文测试 -* ML-KEM-768/1024 diff --git a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/task.json b/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/task.json deleted file mode 100644 index d6d0ec4..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-mlkem-top-tb/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "mlkem-top-tb", - "name": "mlkem-top-tb", - "title": "编写mlkem_top的KAT testbench", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "completedAt": "2026-06-27", - "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-06/06-27-vivado-project-tcl/check.jsonl b/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/check.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/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-06/06-27-vivado-project-tcl/implement.jsonl b/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/implement.jsonl deleted file mode 100644 index 9dd3234..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/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-06/06-27-vivado-project-tcl/prd.md b/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/prd.md deleted file mode 100644 index 67ce52d..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/prd.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vivado 工程创建 TCL 脚本 - -## Goal -编写 `create_project.tcl`,自动创建 Vivado 工程,添加所有 RTL 源文件和 testbench。 - -## Requirements -- 脚本位于项目根目录 -- 自动发现所有 sync_rtl/** 下的 .v 文件 -- 添加 TB 文件(*_xsim.v) -- 设置顶层模块为 tb_mlkem_top_xsim -- 支持 `vivado -mode batch -source create_project.tcl` 运行 diff --git a/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/task.json b/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/task.json deleted file mode 100644 index 603859e..0000000 --- a/.trellis/tasks/archive/2026-06/06-27-vivado-project-tcl/task.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "vivado-project-tcl", - "name": "vivado-project-tcl", - "title": "编写TCL脚本自动创建Vivado工程", - "description": "", - "status": "completed", - "dev_type": null, - "scope": null, - "package": null, - "priority": "P2", - "creator": "FallenSigh", - "assignee": "FallenSigh", - "createdAt": "2026-06-27", - "completedAt": "2026-06-27", - "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 deleted file mode 100644 index 76e8a13..0000000 --- a/.trellis/workflow.md +++ /dev/null @@ -1,690 +0,0 @@ -# Development Workflow - ---- - -## Core Principles - -1. **Plan before code** — figure out what to do before you start -2. **Specs injected, not remembered** — guidelines are injected via hook/skill, not recalled from memory -3. **Persist everything** — research, decisions, and lessons all go to files; conversations get compacted, files don't -4. **Incremental development** — one task at a time -5. **Capture learnings** — after each task, review and write new knowledge back to spec - ---- - -## Trellis System - -### Developer Identity - -On first use, initialize your identity: - -```bash -python3 ./.trellis/scripts/init_developer.py <your-name> -``` - -Creates `.trellis/.developer` (gitignored) + `.trellis/workspace/<your-name>/`. - -### Spec System - -`.trellis/spec/` holds coding guidelines organized by package and layer. - -- `.trellis/spec/<package>/<layer>/index.md` — entry point with **Pre-Development Checklist** + **Quality Check**. Actual guidelines live in the `.md` files it points to. -- `.trellis/spec/guides/index.md` — cross-package thinking guides. - -```bash -python3 ./.trellis/scripts/get_context.py --mode packages # list packages / layers -``` - -**When to update spec**: new pattern/convention found · bug-fix prevention to codify · new technical decision. - -### Task System - -Every task has its own directory under `.trellis/tasks/{MM-DD-name}/` holding `prd.md`, `implement.jsonl`, `check.jsonl`, `task.json`, optional `research/`, `info.md`. - -```bash -# Task lifecycle -python3 ./.trellis/scripts/task.py create "<title>" [--slug <name>] [--parent <dir>] -python3 ./.trellis/scripts/task.py start <name> # set active task (session-scoped when available) -python3 ./.trellis/scripts/task.py current --source # show active task and source -python3 ./.trellis/scripts/task.py finish # clear active task (triggers after_finish hooks) -python3 ./.trellis/scripts/task.py archive <name> # move to archive/{year-month}/ -python3 ./.trellis/scripts/task.py list [--mine] [--status <s>] -python3 ./.trellis/scripts/task.py list-archive - -# Code-spec context (injected into implement/check agents via JSONL). -# `implement.jsonl` / `check.jsonl` are seeded on `task create` for sub-agent-capable -# platforms; the AI curates real spec + research entries during Phase 1.3. -python3 ./.trellis/scripts/task.py add-context <name> <action> <file> <reason> -python3 ./.trellis/scripts/task.py list-context <name> [action] -python3 ./.trellis/scripts/task.py validate <name> - -# Task metadata -python3 ./.trellis/scripts/task.py set-branch <name> <branch> -python3 ./.trellis/scripts/task.py set-base-branch <name> <branch> # PR target -python3 ./.trellis/scripts/task.py set-scope <name> <scope> - -# Hierarchy (parent/child) -python3 ./.trellis/scripts/task.py add-subtask <parent> <child> -python3 ./.trellis/scripts/task.py remove-subtask <parent> <child> - -# PR creation -python3 ./.trellis/scripts/task.py create-pr [name] [--dry-run] -``` - -> Run `python3 ./.trellis/scripts/task.py --help` to see the authoritative, up-to-date list. - -**Current-task mechanism**: `task.py create` creates the task directory and (when session identity is available) auto-sets the per-session active-task pointer so the planning breadcrumb fires immediately. `task.py start` writes the same pointer (idempotent if already set) and flips `task.json.status` from `planning` to `in_progress`. State is stored under `.trellis/.runtime/sessions/`. If no context key is available from hook input, `TRELLIS_CONTEXT_ID`, or a platform-native session environment variable, there is no active task and `task.py start` fails with a session identity hint. `task.py finish` deletes the current session file (status unchanged). `task.py archive <task>` writes `status=completed`, moves the directory to `archive/`, and deletes any runtime session files that still point at the archived task. - -### Workspace System - -Records every AI session for cross-session tracking under `.trellis/workspace/<developer>/`. - -- `journal-N.md` — session log. **Max 2000 lines per file**; a new `journal-(N+1).md` is auto-created when exceeded. -- `index.md` — personal index (total sessions, last active). - -```bash -python3 ./.trellis/scripts/add_session.py --title "Title" --commit "hash" --summary "Summary" -``` - -### Context Script - -```bash -python3 ./.trellis/scripts/get_context.py # full session runtime -python3 ./.trellis/scripts/get_context.py --mode packages # available packages + spec layers -python3 ./.trellis/scripts/get_context.py --mode phase --step <X.Y> # detailed guide for a workflow step -``` - ---- - -<!-- - WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below) - - The 4 [workflow-state:STATUS] blocks embedded in the ## Phase Index section - below are the SINGLE source of truth for the per-turn `<workflow-state>` - breadcrumb that every supported AI platform's UserPromptSubmit hook - reads. inject-workflow-state.py (Python platforms) and - inject-workflow-state.js (OpenCode plugin) only parse them — there is no - fallback dict baked into the scripts after v0.5.0-rc.0. - - STATUS charset: [A-Za-z0-9_-]+. When the hook can't find a tag, it - degrades to a generic "Refer to workflow.md for current step." line — - intentionally visible so users notice and fix a broken workflow.md. - - INVARIANT (test/regression.test.ts): - Every workflow-walkthrough step marked `[required · once]` must have a - matching enforcement line in its phase's [workflow-state:*] block. The - breadcrumb is the only per-turn channel; if a mandatory step isn't - mentioned there, the AI silently skips it (Phase 1.3 jsonl curation - skip and Phase 3.4 commit skip both manifested via this gap). - - TAG ↔ PHASE scoping: - [workflow-state:no_task] → no active task; before Phase 1 - [workflow-state:planning] → all of Phase 1 (status='planning') - [workflow-state:in_progress] → Phase 2 + Phase 3.1-3.4 - (status stays 'in_progress' from - task.py start until task.py archive) - [workflow-state:completed] → currently DEAD: cmd_archive flips - status and moves the dir in the same - call, so the resolver loses the - pointer (block kept for a future - explicit in_progress→completed - transition) - - Editing checklist: - - When you change a [workflow-state:STATUS] block, also check the - matching phase's `[required · once]` walkthrough steps for sync - - Run `trellis update` after editing to push the new bodies to - downstream user projects (block-level managed replacement) - - Full runtime contract: - .trellis/spec/cli/backend/workflow-state-contract.md ---> - -## Phase Index - -``` -Phase 1: Plan → figure out what to do (brainstorm + research → prd.md) -Phase 2: Execute → write code and pass quality checks -Phase 3: Finish → distill lessons + wrap-up -``` - -<!-- Per-turn breadcrumb: shown when there is no active task (before Phase 1) --> - -[workflow-state:no_task] -No active task. **A Direct answer** — pure Q&A / explanation / lookup / chat; no file writes + one-line answer + repo reads ≤ 2 files → AI judges, no override needed. -**B Create a task** — any implementation / code change / build / refactor work. Entry sequence: (1) `python3 ./.trellis/scripts/task.py create "<title>"` to create the task (status=planning, breadcrumb switches to [workflow-state:planning] for brainstorm + jsonl phase guidance) → (2) load `trellis-brainstorm` skill to discuss requirements with the user and iterate on prd.md → (3) once prd is done and jsonl is curated, run `task.py start <task-dir>` to enter [workflow-state:in_progress] for the implementation skeleton. **"It looks small" is NOT grounds for downgrading B to A or C**. -**C Inline change** (per-turn only, escape hatch for B) — the user's CURRENT message MUST contain one of: "skip trellis" / "no task" / "just do it" / "don't create a task" / "跳过 trellis" / "别走流程" / "小修一下" / "直接改" / "先别建任务" → briefly acknowledge ("ok, skipping trellis flow this turn"), then inline. **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said. -[/workflow-state:no_task] - -### Phase 1: Plan -- 1.0 Create task `[required · once]` (just `task.py create`; status enters planning) -- 1.1 Requirement exploration `[required · repeatable]` -- 1.2 Research `[optional · repeatable]` -- 1.3 Configure context `[required · once]` — Claude Code, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi -- 1.4 Activate task `[required · once]` (run `task.py start`; status → in_progress) -- 1.5 Completion criteria - -<!-- Per-turn breadcrumb: shown throughout Phase 1 (status='planning') --> - -[workflow-state:planning] -Load the `trellis-brainstorm` skill and iterate on prd.md with the user. -Phase 1.3 (required, once): before `task.py start`, you MUST curate `implement.jsonl` and `check.jsonl` — list the spec / research files sub-agents need so they get the right context injected. You may skip only if the jsonl already has agent-curated entries (the seed `_example` row alone doesn't count). -Then run `task.py start <task-dir>` to flip status to in_progress. -[/workflow-state:planning] - -<!-- Per-turn breadcrumb: shown throughout Phase 1 when codex.dispatch_mode=inline. - Codex-only opt-in alternate to [workflow-state:planning]. The main agent - edits code directly in Phase 2, so Phase 1.3 jsonl curation is skipped — - the inline workflow loads `trellis-before-dev` instead of injecting JSONL - into a sub-agent. --> - -[workflow-state:planning-inline] -Load the `trellis-brainstorm` skill and iterate on prd.md with the user. -Phase 1.3 jsonl curation is **skipped** in inline dispatch mode — the main session loads `trellis-before-dev` directly in Phase 2 and reads spec context itself, so there is no sub-agent to inject jsonl into. -Then run `task.py start <task-dir>` to flip status to in_progress. -[/workflow-state:planning-inline] - -### Phase 2: Execute -- 2.1 Implement `[required · repeatable]` -- 2.2 Quality check `[required · repeatable]` -- 2.3 Rollback `[on demand]` - -<!-- Per-turn breadcrumb: shown while status='in_progress'. - Scope: all of Phase 2 + Phase 3.1-3.4 (status stays 'in_progress' from - task.py start until task.py archive; only archive flips it). The body - therefore must cover every required step from implementation through - commit, including Phase 3.3 spec update and Phase 3.4 commit. --> - -[workflow-state:in_progress] -**Tools**: `trellis-implement` / `trellis-research` are sub-agent types only (Task/Agent tool, NOT Skill — there is no skill by these names). `trellis-update-spec` is a skill. `trellis-check` exists as both; prefer the Agent form when verifying after code changes. -**Flow**: trellis-implement → trellis-check → trellis-update-spec → commit (Phase 3.4) → `/trellis:finish-work`. -**Main-session default (no override)**: dispatch the `trellis-implement` / `trellis-check` sub-agents — the main agent does NOT edit code by default. Phase 3.4 commit (required, once): after trellis-update-spec, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`). -**Sub-agent self-exemption**: if you are already running as `trellis-implement`, implement directly from the loaded task context and do NOT spawn another `trellis-implement`; if you are already running as `trellis-check`, review/fix directly and do NOT spawn another `trellis-check`. The default dispatch rule applies to the main session only. -**Sub-agent dispatch protocol (all platforms, all sub-agents)**: When you spawn `trellis-implement` / `trellis-check` / `trellis-research`, your dispatch prompt **MUST** start with one line: `Active task: <task path from \`task.py current\`>`. No exceptions. On class-2 platforms (codex / copilot / gemini / qoder) the sub-agent depends on this line because there is no hook to inject task context. On class-1 platforms (claude / cursor / opencode / kiro / codebuddy / droid) the line is normally redundant — the hook injects context directly — but it serves as a critical fallback when the hook fails (Windows + Claude Code PreToolUse silent skip, `--continue` resume, fork distribution, hooks disabled, etc.). For `trellis-research`, the line tells the sub-agent which `{task_dir}/research/` to write into. -**Inline override** (per-turn only, escape hatch for sub-agent dispatch): the user's CURRENT message MUST explicitly contain one of: "do it inline" / "no sub-agent" / "你直接改" / "别派 sub-agent" / "main session 写就行" / "不用 sub-agent". **Without seeing one of these phrases you must NOT inline on your own**; do not invent an override the user never said. -[/workflow-state:in_progress] - -<!-- Per-turn breadcrumb: shown while status='in_progress' when - codex.dispatch_mode=inline. Codex-only opt-in alternate to - [workflow-state:in_progress]. The main session edits code directly - instead of dispatching sub-agents. --> - -[workflow-state:in_progress-inline] -**Flow** (inline mode): main session loads `trellis-before-dev` → main session edits code → main session loads `trellis-check` → run lint / type-check / tests → fix → `trellis-update-spec` → commit (Phase 3.4) → `/trellis:finish-work`. -**Main-session default (inline dispatch_mode)**: the main agent edits code directly. Do NOT dispatch `trellis-implement` / `trellis-check` sub-agents. Load the `trellis-before-dev` skill before writing code; load the `trellis-check` skill before reporting completion. -Phase 3.4 commit (required, once): after `trellis-update-spec`, or whenever implementation is verifiably complete, the main agent **drives the commit** — state the commit plan in user-facing text, then run `git commit` — BEFORE suggesting `/trellis:finish-work`. `/finish-work` refuses to run on a dirty working tree (paths outside `.trellis/workspace/` and `.trellis/tasks/`). -[/workflow-state:in_progress-inline] - -### Phase 3: Finish -- 3.1 Quality verification `[required · repeatable]` -- 3.2 Debug retrospective `[on demand]` -- 3.3 Spec update `[required · once]` -- 3.4 Commit changes `[required · once]` -- 3.5 Wrap-up reminder - -<!-- Per-turn breadcrumb: shown while status='completed'. - Currently DEAD in normal flow: cmd_archive writes status='completed' in - the same call that moves the task dir to archive/, so the active-task - resolver loses the pointer and the hook never fires on archived tasks. - Block preserved for a future status-transition redesign (e.g. an - explicit in_progress→completed command). Edit through the same spec - channel as the live blocks. --> - -[workflow-state:completed] -Code committed via Phase 3.4; run `/trellis:finish-work` to wrap up (archive the task + record session). -If you reach this state with uncommitted code, return to Phase 3.4 first — `/finish-work` refuses to run on a dirty working tree. -`task.py archive` deletes any runtime session files that still point at the archived task. -[/workflow-state:completed] - -### Rules - -1. Identify which Phase you're in, then continue from the next step there -2. Run steps in order inside each Phase; `[required]` steps can't be skipped -3. Phases can roll back (e.g., Execute reveals a prd defect → return to Plan to fix, then re-enter Execute) -4. Steps tagged `[once]` are skipped if the output already exists; don't re-run - -### Skill Routing - -When a user request matches one of these intents, load the corresponding skill (or dispatch the corresponding sub-agent) first — do not skip skills. - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -| User intent | Route | -|---|---| -| Wants a new feature / requirement unclear | `trellis-brainstorm` | -| About to write code / start implementing | Dispatch the `trellis-implement` sub-agent per Phase 2.1 | -| Finished writing / want to verify | Dispatch the `trellis-check` sub-agent per Phase 2.2 | -| Stuck / fixed same bug several times | `trellis-break-loop` | -| Spec needs update | `trellis-update-spec` | - -**Why `trellis-before-dev` is NOT in this table:** you are not the one writing code — the `trellis-implement` sub-agent is. Sub-agent platforms get spec context via `implement.jsonl` injection / prelude, not via the main thread loading `trellis-before-dev`. - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-inline, Kilo, Antigravity, Windsurf] - -| User intent | Skill | -|---|---| -| Wants a new feature / requirement unclear | `trellis-brainstorm` | -| About to write code / start implementing | `trellis-before-dev` (then implement directly in the main session) | -| Finished writing / want to verify | `trellis-check` | -| Stuck / fixed same bug several times | `trellis-break-loop` | -| Spec needs update | `trellis-update-spec` | - -[/codex-inline, Kilo, Antigravity, Windsurf] - -### DO NOT skip skills - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -| What you're thinking | Why it's wrong | -|---|---| -| "This is simple, I'll just code it in the main thread" | Dispatching `trellis-implement` is the cheap path; skipping it tempts you to write code in the main thread and lose spec context — sub-agents get `implement.jsonl` injected, you don't | -| "I already thought it through in plan mode" | Plan-mode output lives in memory — sub-agents can't see it; must be persisted to prd.md | -| "I already know the spec" | The spec may have been updated since you last read it; the sub-agent gets the fresh copy, you may not | -| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper | - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-inline, Kilo, Antigravity, Windsurf] - -| What you're thinking | Why it's wrong | -|---|---| -| "This is simple, just code it" | Simple tasks often grow complex; `trellis-before-dev` takes under a minute and loads the spec context you'll need | -| "I already thought it through in plan mode" | Plan-mode output lives in memory — must be persisted to prd.md before code | -| "I already know the spec" | The spec may have been updated since you last read it; read again | -| "Code first, check later" | `trellis-check` surfaces issues you won't notice yourself; earlier is cheaper | - -[/codex-inline, Kilo, Antigravity, Windsurf] - -### Loading Step Detail - -At each step, run this to fetch detailed guidance: - -```bash -python3 ./.trellis/scripts/get_context.py --mode phase --step <step> -# e.g. python3 ./.trellis/scripts/get_context.py --mode phase --step 1.1 -``` - ---- - -## Phase 1: Plan - -Goal: figure out what to build, produce a clear requirements doc and the context needed to implement it. - -#### 1.0 Create task `[required · once]` - -Create the task directory (status enters `planning`, the session active-task pointer auto-targets the new task when session identity is available): - -```bash -python3 ./.trellis/scripts/task.py create "<task title>" --slug <name> -``` - -`--slug` is the human-readable name only. Do **not** include the `MM-DD-` date prefix; `task.py create` adds that prefix automatically. - -After this command succeeds, the per-turn breadcrumb auto-switches to `[workflow-state:planning]`, telling the AI to enter the brainstorm + jsonl curation phase. - -⚠️ **Run only `create` here — do not also run `start`**. `start` flips status to `in_progress`, which switches the breadcrumb to the implementation phase before brainstorm + jsonl are done — the AI will silently skip them. Save `start` for step 1.4, after jsonl curation is complete. - -Skip when `python3 ./.trellis/scripts/task.py current --source` already points to a task. - -#### 1.1 Requirement exploration `[required · repeatable]` - -Load the `trellis-brainstorm` skill and explore requirements interactively with the user per the skill's guidance. - -The brainstorm skill will guide you to: -- Ask one question at a time -- Prefer researching over asking the user -- Prefer offering options over open-ended questions -- Update `prd.md` immediately after each user answer - -Return to this step whenever requirements change and revise `prd.md`. - -#### 1.2 Research `[optional · repeatable]` - -Research can happen at any time during requirement exploration. It isn't limited to local code — you can use any available tool (MCP servers, skills, web search, etc.) to look up external information, including third-party library docs, industry practices, API references, etc. - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -Spawn the research sub-agent: - -- **Agent type**: `trellis-research` -- **Task description**: Research <specific question> -- **Key requirement**: Research output MUST be persisted to `{TASK_DIR}/research/` - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-inline, Kilo, Antigravity, Windsurf] - -Do the research in the main session directly and write findings into `{TASK_DIR}/research/`. (For `codex-inline` this avoids the `fork_turns="none"` isolation that prevents `trellis-research` sub-agents from resolving the active task path.) - -[/codex-inline, Kilo, Antigravity, Windsurf] - -**Research artifact conventions**: -- One file per research topic (e.g. `research/auth-library-comparison.md`) -- Record third-party library usage examples, API references, version constraints in files -- Note relevant spec file paths you discovered for later reference - -Brainstorm and research can interleave freely — pause to research a technical question, then return to talk with the user. - -**Key principle**: Research output must be written to files, not left only in the chat. Conversations get compacted; files don't. - -#### 1.3 Configure context `[required · once]` - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -Curate `implement.jsonl` and `check.jsonl` so the Phase 2 sub-agents get the right spec context. These files were seeded on `task create` with a single self-describing `_example` line; your job here is to fill in real entries. - -**Location**: `{TASK_DIR}/implement.jsonl` and `{TASK_DIR}/check.jsonl` (already exist). - -**Format**: one JSON object per line — `{"file": "<path>", "reason": "<why>"}`. Paths are repo-root relative. - -**What to put in**: -- **Spec files** — `.trellis/spec/<package>/<layer>/index.md` and any specific guideline files (`error-handling.md`, `conventions.md`, etc.) relevant to this task -- **Research files** — `{TASK_DIR}/research/*.md` that the sub-agent will need to consult - -**What NOT to put in**: -- Code files (`src/**`, `packages/**/*.ts`, etc.) — those are read by the sub-agent during implementation, not pre-registered here -- Files you're about to modify — same reason - -**Split between the two files**: -- `implement.jsonl` → specs + research the implement sub-agent needs to write code correctly -- `check.jsonl` → specs for the check sub-agent (quality guidelines, check conventions, same research if needed) - -**How to discover relevant specs**: - -```bash -python3 ./.trellis/scripts/get_context.py --mode packages -``` - -Lists every package + its spec layers with paths. Pick the entries that match this task's domain. - -**How to append entries**: - -Either edit the jsonl file directly in your editor, or use: - -```bash -python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" implement "<path>" "<reason>" -python3 ./.trellis/scripts/task.py add-context "$TASK_DIR" check "<path>" "<reason>" -``` - -Delete the seed `_example` line once real entries exist (optional — it's skipped automatically by consumers). - -Skip when: `implement.jsonl` has agent-curated entries (the seed row alone doesn't count). - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-inline, Kilo, Antigravity, Windsurf] - -Skip this step. Context is loaded directly by the `trellis-before-dev` skill in Phase 2. - -[/codex-inline, Kilo, Antigravity, Windsurf] - -#### 1.4 Activate task `[required · once]` - -Once prd.md is complete and 1.3 jsonl curation is done, flip the task status to `in_progress`: - -```bash -python3 ./.trellis/scripts/task.py start <task-dir> -``` - -After this command succeeds, the breadcrumb auto-switches to `[workflow-state:in_progress]`, and the rest of Phase 2 / 3 follows. - -If `task.py start` errors with a session-identity message (no context key from hook input, `TRELLIS_CONTEXT_ID`, or platform-native session env), follow the hint in the error to set up session identity, then retry. - -#### 1.5 Completion criteria - -| Condition | Required | -|------|:---:| -| `prd.md` exists | ✅ | -| User confirms requirements | ✅ | -| `task.py start` has been run (status = in_progress) | ✅ | -| `research/` has artifacts (complex tasks) | recommended | -| `info.md` technical design (complex tasks) | optional | - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -| `implement.jsonl` has agent-curated entries (not just the seed row) | ✅ | - -[/Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - ---- - -## Phase 2: Execute - -Goal: turn the prd into code that passes quality checks. - -#### 2.1 Implement `[required · repeatable]` - -[Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -Spawn the implement sub-agent: - -- **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check -- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`. - -The platform hook/plugin auto-handles: -- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt -- Injects prd.md content - -[/Claude Code, Cursor, OpenCode, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -[codex-sub-agent] - -Spawn the implement sub-agent: - -- **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check -- **Dispatch prompt guard**: The prompt MUST start with `Active task: <task path>`, then explicitly say the spawned agent is already `trellis-implement` and must implement directly without spawning another `trellis-implement` / `trellis-check`. - -The Codex sub-agent definition auto-handles the context load requirement: -- Resolves the active task with `task.py current --source`, then reads `prd.md` and `info.md` if present -- Reads `implement.jsonl` and requires the agent to load each referenced spec file before coding - -[/codex-sub-agent] - -[Kiro] - -Spawn the implement sub-agent: - -- **Agent type**: `trellis-implement` -- **Task description**: Implement the requirements per prd.md, consulting materials under `{TASK_DIR}/research/`; finish by running project lint and type-check -- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-implement` sub-agent and must implement directly, not spawn another `trellis-implement` / `trellis-check`. - -The platform prelude auto-handles the context load requirement: -- Reads `implement.jsonl` and injects the referenced spec files into the agent prompt -- Injects prd.md content - -[/Kiro] - -[codex-inline, Kilo, Antigravity, Windsurf] - -1. Load the `trellis-before-dev` skill to read project guidelines -2. Read `{TASK_DIR}/prd.md` for requirements -3. Consult materials under `{TASK_DIR}/research/` -4. Implement the code per requirements -5. Run project lint and type-check - -[/codex-inline, Kilo, Antigravity, Windsurf] - -#### 2.2 Quality check `[required · repeatable]` - -[Claude Code, Cursor, OpenCode, codex-sub-agent, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid, Pi] - -Spawn the check sub-agent: - -- **Agent type**: `trellis-check` -- **Task description**: Review all code changes against spec and prd; fix any findings directly; ensure lint and type-check pass -- **Dispatch prompt guard**: Tell the spawned agent it is already the `trellis-check` sub-agent and must review/fix directly, not spawn another `trellis-check` / `trellis-implement`. - -The check agent's job: -- Review code changes against specs -- 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] - -[codex-inline, Kilo, Antigravity, Windsurf] - -Load the `trellis-check` skill and verify the code per its guidance: -- Spec compliance -- lint / type-check / tests -- Cross-layer consistency (when changes span layers) - -If issues are found → fix → re-check, until green. - -[/codex-inline, Kilo, Antigravity, Windsurf] - -#### 2.3 Rollback `[on demand]` - -- `check` reveals a prd defect → return to Phase 1, fix `prd.md`, then redo 2.1 -- Implementation went wrong → revert code, redo 2.1 -- Need more research → research (same as Phase 1.2), write findings into `research/` - ---- - -## Phase 3: Finish - -Goal: ensure code quality, capture lessons, record the work. - -#### 3.1 Quality verification `[required · repeatable]` - -Load the `trellis-check` skill and do a final verification: -- Spec compliance -- lint / type-check / tests -- Cross-layer consistency (when changes span layers) - -If issues are found → fix → re-check, until green. - -#### 3.2 Debug retrospective `[on demand]` - -If this task involved repeated debugging (the same issue was fixed multiple times), load the `trellis-break-loop` skill to: -- Classify the root cause -- Explain why earlier fixes failed -- Propose prevention - -The goal is to capture debugging lessons so the same class of issue doesn't recur. - -#### 3.3 Spec update `[required · once]` - -Load the `trellis-update-spec` skill and review whether this task produced new knowledge worth recording: -- Newly discovered patterns or conventions -- Pitfalls you hit -- New technical decisions - -Update the docs under `.trellis/spec/` accordingly. Even if the conclusion is "nothing to update", walk through the judgment. - -#### 3.4 Commit changes `[required · once]` - -The AI drives a batched commit of this task's code changes so `/finish-work` can run cleanly afterwards. Goal: produce work commits FIRST, then bookkeeping (archive + journal) commits land after — never interleaved. - -**Step-by-step**: - -1. **Inspect dirty state**: - ```bash - git status --porcelain - ``` - Snapshot every dirty path. If the working tree is clean, skip to 3.5. - -2. **Learn commit style** from recent history (so drafted messages blend in): - ```bash - git log --oneline -5 - ``` - Note the prefix convention (`feat:` / `fix:` / `chore:` / `docs:` ...), language (中文/English), and length style. - -3. **Classify dirty files into two groups**: - - **AI-edited this session** — files you wrote/edited via Edit/Write/Bash tool calls in this session. You know what changed and why. - - **Unrecognized** — dirty files you did NOT touch this session (could be the user's manual edits, leftover WIP from a previous session, or unrelated work). Do NOT silently include these. - -4. **Draft a commit plan**. Group AI-edited files into logical commits (1 commit per coherent change unit, not 1 commit per file). Each entry: `<commit message>` + file list. List unrecognized files separately at the bottom. - -5. **Present the plan once, ask for one-shot confirmation**. Format: - ``` - Proposed commits (in order): - 1. <message> - - <file> - - <file> - 2. <message> - - <file> - - Unrecognized dirty files (NOT in any commit — confirm include/exclude): - - <file> - - <file> - - Reply 'ok' / '行' to execute. Reply with edits, or '我自己来' / 'manual' to abort. - ``` - -6. **On confirmation**: run `git add <files>` + `git commit -m "<msg>"` for each batch in order. Do not amend. Do not push. - -7. **On rejection** (user replies "不行" / "我自己来" / "manual" / any pushback on the plan): stop. Do not attempt a second plan. The user will commit by hand; you skip ahead to 3.5 once they confirm. - -**Rules**: -- No `git commit --amend` anywhere — three-stage three-commit flow (work commits → archive commit → journal commit). -- Never push to remote in this step. -- If the user wants different message wording but accepts the file grouping, edit the message and re-confirm once — but if they reject the grouping, exit to manual mode. -- The batched plan is one prompt; do not prompt per commit. - -#### 3.5 Wrap-up reminder - -After the above, remind the user they can run `/finish-work` to wrap up (archive the task, record the session). - ---- - -## Customizing Trellis (for forks) - -This section is for developers who want to modify the Trellis workflow itself. All customization is done by editing this file; the scripts are parsers only. - -### Changing what a step means - -Edit the corresponding step's walkthrough body in the Phase 1 / 2 / 3 sections above. **Critical constraint**: if you change a step's `[required · once]` marker or add a new `[required · once]` step, you MUST also add a matching enforcement line to that phase's `[workflow-state:STATUS]` tag block — otherwise the per-turn breadcrumb omits the reinforcement, and the AI silently skips the step. The regression tests assert this. - -All 4 tag blocks live in the `## Phase Index` section above, immediately after each phase summary: - -| Scope | Corresponding tag | -|---|---| -| No active task (before Phase 1) | `[workflow-state:no_task]` (after the Phase Index ASCII art) | -| All of Phase 1 (task created → ready for implementation) | `[workflow-state:planning]` (after Phase 1 summary) | -| Phase 2 + Phase 3.1–3.4 (implementation + check + wrap-up) | `[workflow-state:in_progress]` (after Phase 2 summary) | -| After Phase 3.5 (archived) | `[workflow-state:completed]` (after Phase 3 summary; **currently DEAD**) | - -### Changing the per-turn prompt text - -Directly edit the body of the corresponding `[workflow-state:STATUS]` block. After editing, run `trellis update` (if you're a template maintainer) or restart your AI session (if you're customizing your own project) — no script changes required. - -### Adding a custom status - -Add a new block: - -``` -[workflow-state:my-status] -your per-turn prompt text -[/workflow-state:my-status] -``` - -Constraints: -- STATUS charset: `[A-Za-z0-9_-]+` (underscores and hyphens allowed, e.g. `in-review`, `blocked-by-team`) -- A lifecycle hook must write `task.json.status` to your custom value, otherwise the tag is never read -- Lifecycle hooks live in `task.json.hooks.after_*` and bind to one of `after_create / after_start / after_finish / after_archive` - -### Adding a lifecycle hook - -Add a `hooks` field to your `task.json`: - -```json -{ - "hooks": { - "after_finish": [ - "your-script-or-command-here" - ] - } -} -``` - -Supported events: `after_create / after_start / after_finish / after_archive`. Note that `after_finish` ≠ a status change (it only clears the active-task pointer); use `after_archive` for "task is done" notifications. - -### Full contract - -For the workflow state machine's runtime contract, the locations of all status writers, pseudo-statuses (`no_task` / `stale_<source_type>`), the hook reachability matrix, and other deep details, see: - -- `.trellis/spec/cli/backend/workflow-state-contract.md` — runtime contract + writer table + test invariants -- `.trellis/scripts/inject-workflow-state.py` — actual parser (reads workflow.md only, no embedded text) diff --git a/.trellis/workspace/FallenSigh/index.md b/.trellis/workspace/FallenSigh/index.md deleted file mode 100644 index 2f02d2c..0000000 --- a/.trellis/workspace/FallenSigh/index.md +++ /dev/null @@ -1,42 +0,0 @@ -# Workspace Index - FallenSigh - -> Journal tracking for AI development sessions. - ---- - -## Current Status - -<!-- @@@auto:current-status --> -- **Active File**: `journal-1.md` -- **Total Sessions**: 2 -- **Last Active**: 2026-06-25 -<!-- @@@/auto:current-status --> - ---- - -## Active Documents - -<!-- @@@auto:active-documents --> -| File | Lines | Status | -|------|-------|--------| -| `journal-1.md` | ~76 | Active | -<!-- @@@/auto:active-documents --> - ---- - -## Session History - -<!-- @@@auto:session-history --> -| # | Date | Title | Commits | Branch | -|---|------|-------|---------|--------| -| 2 | 2026-06-25 | Fix 7 failing Vivado XSIM testbenches | `f5365c9` | `main` | -| 1 | 2026-06-25 | Add Vivado XSIM Verilog testbenches for all 10 sync modules | `d4c3fc8`, `52c625b`, `79653ac`, `db0a559` | `main` | -<!-- @@@/auto:session-history --> - ---- - -## Notes - -- Sessions are appended to journal files -- New journal file created when current exceeds 2000 lines -- Use `add_session.py` to record sessions \ No newline at end of file diff --git a/.trellis/workspace/FallenSigh/journal-1.md b/.trellis/workspace/FallenSigh/journal-1.md deleted file mode 100644 index 1aba56c..0000000 --- a/.trellis/workspace/FallenSigh/journal-1.md +++ /dev/null @@ -1,76 +0,0 @@ -# Journal - FallenSigh (Part 1) - -> AI development session journal -> Started: 2026-06-24 - ---- - - - -## Session 1: Add Vivado XSIM Verilog testbenches for all 10 sync modules - -**Date**: 2026-06-25 -**Task**: Add Vivado XSIM Verilog testbenches for all 10 sync modules -**Branch**: `main` - -### Summary - -Created file-based vector Verilog testbenches () for all 10 top-level sync modules: mod_add, rng, poly_arith, comp_decomp, storage, sha3_chain, ntt_core, poly_mul, sample_cbd, sample_ntt. Each module includes tb .v, gen_vectors.py, input.hex, xsim_run.tcl. Added run_tb.sh convenience script. Verified on Vivado 2019.2 with ncurses compatibility fix. - -### Main Changes - -(Add details) - -### Git Commits - -| Hash | Message | -|------|---------| -| `d4c3fc8` | (see git log) | -| `52c625b` | (see git log) | -| `79653ac` | (see git log) | -| `db0a559` | (see git log) | - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete - - -## Session 2: Fix 7 failing Vivado XSIM testbenches - -**Date**: 2026-06-25 -**Task**: Fix 7 failing Vivado XSIM testbenches -**Branch**: `main` - -### Summary - -Fixed 7 testbench failures on Vivado 2019.2: (1) sha3_top.v declaration ordering, (2) TCL variable paths + --relax flag, (3) Verilog part-select changed to +: operator, (4) BRAM read latency timing, (5) comp_decomp d=12 edge case, (6) sample_ntt TB timing bug (DUT Keccak pipeline drain). All 10 modules now pass. - -### Main Changes - -(Add details) - -### Git Commits - -| Hash | Message | -|------|---------| -| `f5365c9` | (see git log) | - -### Testing - -- [OK] (Add test results) - -### Status - -[OK] **Completed** - -### Next Steps - -- None - task complete diff --git a/.trellis/workspace/index.md b/.trellis/workspace/index.md deleted file mode 100644 index f132a77..0000000 --- a/.trellis/workspace/index.md +++ /dev/null @@ -1,125 +0,0 @@ -# Workspace Index - -> Records of all AI Agent work records across all developers - ---- - -## Overview - -This directory tracks records for all developers working with AI Agents on this project. - -### File Structure - -``` -workspace/ -|-- index.md # This file - main index -+-- {developer}/ # Per-developer directory - |-- index.md # Personal index with session history - |-- tasks/ # Task files - | |-- *.json # Active tasks - | +-- archive/ # Archived tasks by month - +-- journal-N.md # Journal files (sequential: 1, 2, 3...) -``` - ---- - -## Active Developers - -| Developer | Last Active | Sessions | Active File | -|-----------|-------------|----------|-------------| -| (none yet) | - | - | - | - ---- - -## Getting Started - -### For New Developers - -Run the initialization script: - -```bash -python3 ./.trellis/scripts/init_developer.py <your-name> -``` - -This will: -1. Create your identity file (gitignored) -2. Create your progress directory -3. Create your personal index -4. Create initial journal file - -### For Returning Developers - -1. Get your developer name: - ```bash - python3 ./.trellis/scripts/get_developer.py - ``` - -2. Read your personal index: - ```bash - cat .trellis/workspace/$(python3 ./.trellis/scripts/get_developer.py)/index.md - ``` - ---- - -## Guidelines - -### Journal File Rules - -- **Max 2000 lines** per journal file -- When limit is reached, create `journal-{N+1}.md` -- Update your personal `index.md` when creating new files - -### Session Record Format - -Each session should include: -- Summary: One-line description -- Branch: Which branch the work was done on -- Main Changes: What was modified -- Git Commits: Commit hashes and messages -- Next Steps: What to do next - ---- - -## Session Template - -Use this template when recording sessions: - -```markdown -## Session {N}: {Title} - -**Date**: YYYY-MM-DD -**Task**: {task-name} -**Branch**: `{branch-name}` - -### Summary - -{One-line summary} - -### Main Changes - -- {Change 1} -- {Change 2} - -### Git Commits - -| Hash | Message | -|------|---------| -| `abc1234` | {commit message} | - -### Testing - -- [OK] {Test result} - -### Status - -[OK] **Completed** / # **In Progress** / [P] **Blocked** - -### Next Steps - -- {Next step 1} -- {Next step 2} -``` - ---- - -**Language**: All documentation must be written in **English**. diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c9c4c66..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,21 +0,0 @@ -<!-- TRELLIS:START --> -# Trellis Instructions - -These instructions are for AI assistants working in this project. - -This project is managed by Trellis. The working knowledge you need lives under `.trellis/`: - -- `.trellis/workflow.md` — development phases, when to create tasks, skill routing -- `.trellis/spec/` — package- and layer-scoped coding guidelines (read before writing code in a given layer) -- `.trellis/workspace/` — per-developer journals and session traces -- `.trellis/tasks/` — active and archived tasks (PRDs, research, jsonl context) - -If a Trellis command is available on your platform (e.g. `/trellis:finish-work`, `/trellis:continue`), prefer it over manual steps. Not every platform exposes every command. - -If you're using Codex or another agent-capable tool, additional project-scoped helpers may live in: -- `.agents/skills/` — reusable Trellis skills -- `.codex/agents/` — optional custom subagents - -Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`. - -<!-- TRELLIS:END -->