feat(workflow): per-task workflow selection via .trellis/workflows library - #467
Conversation
…brary A task can now pin a workflow variant instead of every task sharing the single global .trellis/workflow.md: - New .trellis/workflows/<id>.md library, populated by `trellis workflow --save <id>` (marketplace-aware, --force overwrite gate, warn-only marker validation; never touches workflow.md or .template-hashes.json — library files are user-managed and invisible to `trellis update`). - task.json gains an optional "workflow" field: `task.py create --workflow <id>` and `task.py workflow <id> | --clear`. - Single resolution rule in scripts/common/workflow_selection.py (valid id + existing library file -> variant; otherwise stderr warn + global fallback; never raises), consumed by session-start.py (shared + codex/ copilot/cursor copies), inject-workflow-state.py, workflow_phase.py (--mode phase), and the OpenCode inject-workflow-state.js port. - Absent the field, every consumer's output is byte-identical to before. - Specs updated: commands-workflow.md (--save + library ownership), workflow-state-contract.md (per-task resolution order; Pi/OMP parity tracked as follow-up). - Dogfood mirrors patched surgically (.claude/.codex/.cursor hooks, .trellis/scripts, .opencode plugin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis change adds task-scoped workflow variants under ChangesPer-task workflow selection
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant WorkflowCLI
participant TaskJSON
participant RuntimeHook
participant WorkflowFile
User->>WorkflowCLI: save workflow variant
WorkflowCLI->>WorkflowFile: write .trellis/workflows/<id>.md
User->>TaskJSON: assign workflow id
RuntimeHook->>TaskJSON: read active task selection
RuntimeHook->>WorkflowFile: load selected workflow
WorkflowFile-->>RuntimeHook: provide phase and breadcrumb content
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/core/test/task/schema.test.ts (1)
74-87: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueTest matches current schema; schema itself doesn't enforce the
WORKFLOW_ID_REid format.
taskRecordSchemaonly checks thatworkflowis a string, not that it matches the identifier grammar (letters, digits, '-', '_') enforced on the Python side (task.py,workflow_selection.py). Any TS-side writer oftask.jsoncould persist a syntactically invalid id; the Python resolver would still safely fall back with a warning, but it's a cross-layer format contract gap worth closing for defense-in-depth.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/test/task/schema.test.ts` around lines 74 - 87, The workflow field test only validates string typing; update taskRecordSchema’s workflow validation to also enforce the shared workflow identifier grammar represented by WORKFLOW_ID_RE, allowing only letters, digits, hyphens, and underscores. Extend the schema test around the optional workflow selection to reject syntactically invalid string IDs while preserving valid IDs such as “tdd” and the existing non-string rejection.packages/cli/src/templates/opencode/plugins/inject-workflow-state.js (1)
268-269: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDuplicate active-task resolution on every chat message.
resolveWorkflowMdandgetActiveTaskare called back-to-back and each independently callsctx.getActiveTask(...), resolves the task directory, and readstask.json. In this long-lived plugin process, that doubles this I/O/lookup work on every single message rather than once per turn as before.
packages/cli/src/templates/opencode/plugins/inject-workflow-state.js#L268-L269: resolve the active task once (e.g., haveresolveWorkflowMdaccept the already-resolved active-task/task-dir data, or merge both lookups into a single helper that returns both the task info and the workflow path) and reuse it for bothtemplatesandtask..opencode/plugins/inject-workflow-state.js#L178-L179: apply the same consolidation here, since this file mirrors the canonical template.♻️ Sketch of the consolidation
- const templates = loadBreadcrumbs(resolveWorkflowMd(ctx, directory, input)) - const task = getActiveTask(ctx, input) + const active = resolveActiveTaskState(ctx, input) // single ctx.getActiveTask + task dir + task.json read + const templates = loadBreadcrumbs(resolveWorkflowMdFromState(directory, active)) + const task = taskFromState(active)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/templates/opencode/plugins/inject-workflow-state.js` around lines 268 - 269, Consolidate active-task resolution so each chat message performs the ctx.getActiveTask, task-directory resolution, and task.json read only once, then reuse the resulting task data for both templates and task in resolveWorkflowMd/getActiveTask flow. Apply the same change at packages/cli/src/templates/opencode/plugins/inject-workflow-state.js:268-269 and its mirrored .opencode/plugins/inject-workflow-state.js:178-179; keep both files’ behavior otherwise unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.trellis/spec/cli/backend/commands-workflow.md:
- Around line 304-315: Rename the repeated Markdown headings in the new --save
examples from “#### Wrong” and “#### Correct” to unique headings such as “####
Wrong: --save” and “#### Correct: --save” so MD024 passes and generated
navigation is unambiguous.
In @.trellis/spec/cli/backend/workflow-state-contract.md:
- Around line 24-26: Update the source-of-truth wording in the workflow-state
contract, including the referenced statements near the breadcrumb-body
description and later editable-source statement, to consistently identify both
the global workflow and the active task’s selected variant. Remove claims that
.trellis/workflow.md is the sole location while preserving the surrounding
parser, writer, lifecycle, and reachability scope.
In @.trellis/tasks/07-23-per-task-workflow/prd.md:
- Line 58: In the documented parser-marker text, remove the trailing space
inside the inline code span around the step-heading marker, changing the
represented marker to exactly `####`. Preserve the surrounding requirements,
including `## Phase Index` and the requirement for at least one step heading.
In `@packages/cli/src/templates/trellis/scripts/common/workflow_selection.py`:
- Around line 60-62: Update the workflow selection logic around workflow_id so a
missing "workflow" key continues to return fallback silently, while an
explicitly present value that is empty, null, or non-string emits exactly one
warning to stderr before returning fallback. Preserve the existing valid string
selection behavior.
---
Nitpick comments:
In `@packages/cli/src/templates/opencode/plugins/inject-workflow-state.js`:
- Around line 268-269: Consolidate active-task resolution so each chat message
performs the ctx.getActiveTask, task-directory resolution, and task.json read
only once, then reuse the resulting task data for both templates and task in
resolveWorkflowMd/getActiveTask flow. Apply the same change at
packages/cli/src/templates/opencode/plugins/inject-workflow-state.js:268-269 and
its mirrored .opencode/plugins/inject-workflow-state.js:178-179; keep both
files’ behavior otherwise unchanged.
In `@packages/core/test/task/schema.test.ts`:
- Around line 74-87: The workflow field test only validates string typing;
update taskRecordSchema’s workflow validation to also enforce the shared
workflow identifier grammar represented by WORKFLOW_ID_RE, allowing only
letters, digits, hyphens, and underscores. Extend the schema test around the
optional workflow selection to reject syntactically invalid string IDs while
preserving valid IDs such as “tdd” and the existing non-string rejection.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 26b3a6c9-8cd6-48e7-a58d-0f1bfc6ca91c
📒 Files selected for processing (41)
.claude/hooks/inject-workflow-state.py.claude/hooks/session-start.py.codex/hooks/inject-workflow-state.py.codex/hooks/session-start.py.cursor/hooks/session-start.py.opencode/plugins/inject-workflow-state.js.trellis/scripts/common/task_store.py.trellis/scripts/common/types.py.trellis/scripts/common/workflow_phase.py.trellis/scripts/common/workflow_selection.py.trellis/scripts/task.py.trellis/spec/cli/backend/commands-workflow.md.trellis/spec/cli/backend/workflow-state-contract.md.trellis/tasks/07-23-per-task-workflow/check.jsonl.trellis/tasks/07-23-per-task-workflow/design.md.trellis/tasks/07-23-per-task-workflow/implement.jsonl.trellis/tasks/07-23-per-task-workflow/implement.md.trellis/tasks/07-23-per-task-workflow/prd.md.trellis/tasks/07-23-per-task-workflow/task.json.trellis/workspace/tommy/index.md.trellis/workspace/tommy/journal-1.mdpackages/cli/src/cli/index.tspackages/cli/src/commands/workflow.tspackages/cli/src/templates/codex/hooks/session-start.pypackages/cli/src/templates/copilot/hooks/session-start.pypackages/cli/src/templates/opencode/plugins/inject-workflow-state.jspackages/cli/src/templates/shared-hooks/inject-workflow-state.pypackages/cli/src/templates/shared-hooks/session-start.pypackages/cli/src/templates/trellis/index.tspackages/cli/src/templates/trellis/scripts/common/task_store.pypackages/cli/src/templates/trellis/scripts/common/types.pypackages/cli/src/templates/trellis/scripts/common/workflow_phase.pypackages/cli/src/templates/trellis/scripts/common/workflow_selection.pypackages/cli/src/templates/trellis/scripts/task.pypackages/cli/test/commands/workflow.integration.test.tspackages/cli/test/scripts/inject-workflow-state-kiro.integration.test.tspackages/cli/test/templates/opencode.test.tspackages/core/src/task/records.tspackages/core/src/task/schema.tspackages/core/test/task/records.test.tspackages/core/test/task/schema.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts (1)
230-255: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCover global fallback behavior, not only side effects.
Both tests omit the runtime assertion that this feature requires:
packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts#L230-L255: assert that invalid values produce the global workflow’s distinctive marker.packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts#L199-L228: after--clear, rerun the hook and assert the global marker appears instead of the selected marker.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts` around lines 230 - 255, The invalid workflow test at packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts:230-255 must assert that the hook output contains the global workflow’s distinctive marker, in addition to warning and exit-status checks. The --clear test at packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts:199-228 must rerun the hook after clearing and assert the global marker appears instead of the selected workflow marker.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts`:
- Around line 230-255: The invalid workflow test at
packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts:230-255
must assert that the hook output contains the global workflow’s distinctive
marker, in addition to warning and exit-status checks. The --clear test at
packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts:199-228
must rerun the hook after clearing and assert the global marker appears instead
of the selected workflow marker.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2435ce42-2f84-475d-84b8-01fdd506d0f3
📒 Files selected for processing (11)
.opencode/plugins/inject-workflow-state.js.trellis/scripts/common/workflow_selection.py.trellis/spec/cli/backend/commands-workflow.md.trellis/spec/cli/backend/workflow-state-contract.md.trellis/tasks/07-23-per-task-workflow/prd.mdpackages/cli/src/templates/opencode/plugins/inject-workflow-state.jspackages/cli/src/templates/trellis/scripts/common/workflow_selection.pypackages/cli/test/scripts/inject-workflow-state-kiro.integration.test.tspackages/cli/test/templates/opencode.test.tspackages/core/src/task/schema.tspackages/core/test/task/schema.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- packages/cli/test/templates/opencode.test.ts
- .trellis/tasks/07-23-per-task-workflow/prd.md
- .trellis/spec/cli/backend/commands-workflow.md
- .trellis/scripts/common/workflow_selection.py
- .trellis/spec/cli/backend/workflow-state-contract.md
What
A task can now pin a workflow variant instead of every task sharing the single global
.trellis/workflow.md:.trellis/workflows/<id>.md, populated bytrellis workflow --save <id>(marketplace-aware,--forceoverwrite gate, warn-only marker validation).--savenever touchesworkflow.mdor.template-hashes.json— library files are user-managed and invisible totrellis update(same ownership stance as a non-native global workflow)."workflow"field intask.json, set viatask.py create --workflow <id>ortask.py workflow <id> | --clear.scripts/common/workflow_selection.py(valid id + existing library file → variant; selection present but file missing → stderr warn + global fallback; otherwise global; never raises), consumed by every runtime injection path:session-start.py(shared + codex/copilot/cursor copies) — SessionStart Phase Indexinject-workflow-state.py— per-turn breadcrumbsworkflow_phase.py/get_context.py --mode phase— step detailinject-workflow-state.jsportcommands-workflow.md(--save + library ownership contract),workflow-state-contract.md(per-task resolution order)..claude/.codex/.cursorhooks,.trellis/scripts,.opencodeplugin).Why
Project-level switching shipped in the workflow-marketplace work (May 2026), but all tasks and sessions in a project still share one workflow. Teams want e.g. the TDD workflow for feature tasks and native for quick fixes without flipping the workflow globally for everyone mid-flight. Selection is stored per task (not config) deliberately — it respects the recorded non-goal of "no long-lived workflow.variant config key": task lifecycle bounds the state, and the global file + hash contract are untouched.
Verification
--save/ library listing / hash-non-mutation / marker warnings / update-safety; full suite green (pre-commit runs it).tddtemplate: save → create task --workflow tdd → SessionStart/breadcrumb/--mode phase all serve TDD content →--clearreturns native.Known follow-ups (documented in workflow-state-contract.md)
.trellis/workflows/namespace to avoid Migrate Trellis workflow to YAML manifest #337's proposed.trellis/workflow/, and the resolver is one function to re-point during any future format migration.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
.trellis/workflows/<id>.md, listing saved variants, and set/clear selection on tasks.Documentation
--savebehavior.Tests
--save, overwrite rules, marker warnings, invalid-id validation, and ensuring updates don’t modify saved workflow libraries.