Skip to content

feat(workflow): per-task workflow selection via .trellis/workflows library - #467

Merged
taosu0216 merged 4 commits into
mainfrom
feat/per-task-workflow-selection
Jul 30, 2026
Merged

feat(workflow): per-task workflow selection via .trellis/workflows library#467
taosu0216 merged 4 commits into
mainfrom
feat/per-task-workflow-selection

Conversation

@Zwdommy

@Zwdommy Zwdommy commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What

A task can now pin a workflow variant instead of every task sharing the single global .trellis/workflow.md:

  • Workflow library: .trellis/workflows/<id>.md, populated by trellis workflow --save <id> (marketplace-aware, --force overwrite gate, warn-only marker validation). --save never touches workflow.md or .template-hashes.json — library files are user-managed and invisible to trellis update (same ownership stance as a non-native global workflow).
  • Per-task selection: optional "workflow" field in task.json, set via task.py create --workflow <id> or task.py workflow <id> | --clear.
  • Single resolution rule in 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 Index
    • inject-workflow-state.py — per-turn breadcrumbs
    • workflow_phase.py / get_context.py --mode phase — step detail
    • OpenCode inject-workflow-state.js port
  • Absent the field, every consumer's output is byte-identical to today.
  • Specs updated: commands-workflow.md (--save + library ownership contract), workflow-state-contract.md (per-task resolution order).
  • Dogfood mirrors patched surgically (.claude/.codex/.cursor hooks, .trellis/scripts, .opencode plugin).

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

  • 18 integration tests for --save / library listing / hash-non-mutation / marker warnings / update-safety; full suite green (pre-commit runs it).
  • E2E against the live marketplace tdd template: save → create task --workflow tdd → SessionStart/breadcrumb/--mode phase all serve TDD content → --clear returns native.
  • trellis-check pass over the full diff (resolution-rule identity verified across all 4 implementations, mirror parity verified mechanically).

Known follow-ups (documented in workflow-state-contract.md)

  • Pi / OMP extensions still inject the global workflow (their reads sit inside monolithic TS extensions) — tracked follow-up.
  • Relationship to Migrate Trellis workflow to YAML manifest #337 (workflow YAML manifest): no conflict by design — this stays on monolithic markdown, uses the plural .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

    • Added per-task workflow variants so a task can select its own workflow markdown at runtime.
    • Added workflow tooling, including saving variants to .trellis/workflows/<id>.md, listing saved variants, and set/clear selection on tasks.
    • Session context, phase guidance, and workflow-state breadcrumbs now reflect the selected variant (with safe fallback).
  • Documentation

    • Updated workflow-state contracts and CLI workflow documentation for per-task resolution and --save behavior.
  • Tests

    • Added integration/unit coverage for --save, overwrite rules, marker warnings, invalid-id validation, and ensuring updates don’t modify saved workflow libraries.

…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>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds task-scoped workflow variants under .trellis/workflows/, commands for assigning or clearing variants, shared runtime resolution with global fallback, CLI saving support, and coverage across hooks, plugins, task records, tests, and specifications.

Changes

Per-task workflow selection

Layer / File(s) Summary
Workflow resolution and task schema
.trellis/scripts/common/*, packages/cli/src/templates/trellis/scripts/common/*, packages/core/src/task/*
Workflow IDs, task JSON fields, variant lookup, fallback behavior, and phase loading now support task-selected workflow files.
Task workflow assignment commands
.trellis/scripts/task.py, packages/cli/src/templates/trellis/scripts/task.py
Task creation accepts --workflow; a new workflow <id> command sets the active task variant and --clear removes it.
Runtime workflow consumers
.claude/hooks/*, .codex/hooks/*, .cursor/hooks/*, .opencode/plugins/*, packages/cli/src/templates/*
Session-start overviews and workflow-state breadcrumbs resolve the active task workflow, falling back to .trellis/workflow.md.
Workflow library save CLI
packages/cli/src/cli/index.ts, packages/cli/src/commands/workflow.ts
trellis workflow --save <id> writes library variants, supports listing and --force, validates IDs, and emits marker warnings.
Validation and task records
packages/cli/test/*, packages/core/test/task/*
Tests cover library save behavior, task selection and clearing, runtime output, OpenCode resolution, and persisted workflow fields.
Specifications and task artifacts
.trellis/spec/*, .trellis/tasks/07-23-per-task-workflow/*, .trellis/workspace/tommy/*
Contracts, design notes, implementation plans, task metadata, and workspace journal files document workflow-selection behavior.

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
Loading

Possibly related PRs

Suggested reviewers: taosu0216

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: per-task workflow selection backed by the .trellis/workflows library.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/per-task-workflow-selection

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@taosu0216
taosu0216 marked this pull request as ready for review July 30, 2026 08:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/core/test/task/schema.test.ts (1)

74-87: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Test matches current schema; schema itself doesn't enforce the WORKFLOW_ID_RE id format.

taskRecordSchema only checks that workflow is 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 of task.json could 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 win

Duplicate active-task resolution on every chat message. resolveWorkflowMd and getActiveTask are called back-to-back and each independently calls ctx.getActiveTask(...), resolves the task directory, and reads task.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., have resolveWorkflowMd accept 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 both templates and task.
  • .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

📥 Commits

Reviewing files that changed from the base of the PR and between e4ed585 and c7f1567.

📒 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.md
  • packages/cli/src/cli/index.ts
  • packages/cli/src/commands/workflow.ts
  • packages/cli/src/templates/codex/hooks/session-start.py
  • packages/cli/src/templates/copilot/hooks/session-start.py
  • packages/cli/src/templates/opencode/plugins/inject-workflow-state.js
  • packages/cli/src/templates/shared-hooks/inject-workflow-state.py
  • packages/cli/src/templates/shared-hooks/session-start.py
  • packages/cli/src/templates/trellis/index.ts
  • packages/cli/src/templates/trellis/scripts/common/task_store.py
  • packages/cli/src/templates/trellis/scripts/common/types.py
  • packages/cli/src/templates/trellis/scripts/common/workflow_phase.py
  • packages/cli/src/templates/trellis/scripts/common/workflow_selection.py
  • packages/cli/src/templates/trellis/scripts/task.py
  • packages/cli/test/commands/workflow.integration.test.ts
  • packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts
  • packages/cli/test/templates/opencode.test.ts
  • packages/core/src/task/records.ts
  • packages/core/src/task/schema.ts
  • packages/core/test/task/records.test.ts
  • packages/core/test/task/schema.test.ts

Comment thread .trellis/spec/cli/backend/commands-workflow.md Outdated
Comment thread .trellis/spec/cli/backend/workflow-state-contract.md
Comment thread .trellis/tasks/07-23-per-task-workflow/prd.md Outdated
Comment thread packages/cli/src/templates/trellis/scripts/common/workflow_selection.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts (1)

230-255: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover 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

📥 Commits

Reviewing files that changed from the base of the PR and between c7f1567 and 1a3571f.

📒 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.md
  • packages/cli/src/templates/opencode/plugins/inject-workflow-state.js
  • packages/cli/src/templates/trellis/scripts/common/workflow_selection.py
  • packages/cli/test/scripts/inject-workflow-state-kiro.integration.test.ts
  • packages/cli/test/templates/opencode.test.ts
  • packages/core/src/task/schema.ts
  • packages/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

@taosu0216
taosu0216 merged commit c3596dd into main Jul 30, 2026
2 checks passed
taosu0216 added a commit that referenced this pull request Jul 30, 2026
* revert: keep per-task workflow selection on beta

This reverts commit c3596dd (#467), which targeted main but belongs to the v0.7 beta release line.

* docs: make workflow hash example verifiable

---------

Co-authored-by: taosu <taosu@mindfold.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants