feat(trellis): enhance bundled skills and workflow integration
- Updated bundled skills documentation to clarify the structure and usage across all platforms, ensuring consistency in skill root paths. - Introduced a new `inject-spec-context.py` hook for path-scoped spec context injection, improving the relevance of injected specs during file interactions. - Enhanced existing hooks to support workflow resolution, allowing for dynamic selection of workflows based on task context. - Added a command to manage workflow selections for active tasks, enabling better task management and workflow adherence. - Updated configuration options for spec injection, including character limits and refresh windows, to optimize performance and usability.
This commit is contained in:
Regular → Executable
+52
-16
@@ -10,19 +10,24 @@ The emitted ``hookEventName`` field is platform-aware: most hosts expect
|
||||
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
|
||||
Breadcrumb text is pulled exclusively from the resolved workflow file's
|
||||
[workflow-state:STATUS] tag blocks — the active task may select a
|
||||
per-task variant (`.trellis/workflows/<id>.md` via task.json `workflow`),
|
||||
otherwise personal, team, and global defaults are resolved in order.
|
||||
There are no fallback dicts in this script: when the resolved workflow is
|
||||
missing or a tag is absent, the breadcrumb degrades to a generic
|
||||
"Refer to workflow.md for current step." line so users see (and fix)
|
||||
the broken state instead of the hook silently masking it.
|
||||
|
||||
Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder,
|
||||
CodeBuddy, Droid, Gemini, Copilot, Kiro). Kiro wires this via the CLI
|
||||
Which platforms register this hook is decided by SHARED_HOOKS_BY_PLATFORM
|
||||
in templates/shared-hooks/index.ts — currently Claude, Codex, Gemini,
|
||||
Qoder, Copilot, CodeBuddy, Droid, Kiro, Trae and ZCode. That table is the
|
||||
source of truth; each listed platform's collect<Platform>Templates() pulls
|
||||
this file into its template map through collectSharedHooks(), and a single
|
||||
writer puts that map on disk at init time. Kiro wires this via the CLI
|
||||
custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook``
|
||||
``promptSubmit`` event; its output branch emits a plain-text breadcrumb
|
||||
(Kiro adds hook stdout directly to the conversation context). Written to
|
||||
each platform's hooks directory via writeSharedHooks() at init time.
|
||||
(Kiro adds hook stdout directly to the conversation context).
|
||||
|
||||
Silent exit 0 cases (no output):
|
||||
- No .trellis/ directory found (not a Trellis project)
|
||||
@@ -95,11 +100,16 @@ def find_trellis_root(start: Path) -> Optional[Path]:
|
||||
def _detect_platform(input_data: dict) -> str | None:
|
||||
if isinstance(input_data.get("cursor_version"), str):
|
||||
return "cursor"
|
||||
# CLAUDE_PROJECT_DIR is a compatibility alias that several hosts set
|
||||
# alongside their own variable — CodeBuddy, ZCode and Trae all do. It must
|
||||
# therefore be checked LAST, or every one of them is detected as claude and
|
||||
# the context key becomes `claude_<their-session-id>`. That key does not
|
||||
# match the session file `task.py start` wrote under the host's real name,
|
||||
# so every turn reports no_task while the pointer exists on disk.
|
||||
# Observed on CodeBuddy IDE 4.10.4: session file `codebuddy_ae54840e….json`
|
||||
# alongside marker `update-check-claude_ae54840e….marker`, same id.
|
||||
env_map = {
|
||||
# ZCode may set both ZCODE_PROJECT_DIR and CLAUDE_PROJECT_DIR; check
|
||||
# ZCODE first so ZCode sessions aren't misdetected as claude.
|
||||
"ZCODE_PROJECT_DIR": "zcode",
|
||||
"CLAUDE_PROJECT_DIR": "claude",
|
||||
"CURSOR_PROJECT_DIR": "cursor",
|
||||
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
||||
"FACTORY_PROJECT_DIR": "droid",
|
||||
@@ -108,6 +118,8 @@ def _detect_platform(input_data: dict) -> str | None:
|
||||
"KIRO_PROJECT_DIR": "kiro",
|
||||
"COPILOT_PROJECT_DIR": "copilot",
|
||||
"TRAE_PROJECT_DIR": "trae",
|
||||
# Last: the shared alias, only meaningful once no vendor key matched.
|
||||
"CLAUDE_PROJECT_DIR": "claude",
|
||||
}
|
||||
for env_name, platform in env_map.items():
|
||||
if os.environ.get(env_name):
|
||||
@@ -183,16 +195,40 @@ _TAG_RE = re.compile(
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def load_breadcrumbs(root: Path) -> dict[str, str]:
|
||||
"""Parse workflow.md for [workflow-state:STATUS] blocks.
|
||||
def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
|
||||
"""Resolve the active task's workflow file, falling back to the global one.
|
||||
|
||||
Returns {status: body_text}. workflow.md is the single source of
|
||||
The per-task resolution rule lives in common.workflow_selection inside
|
||||
.trellis/scripts. Older installed projects may not ship that module, and
|
||||
hooks must never crash the session — ANY failure (import error, old
|
||||
scripts tree, resolver bug) falls back to the global workflow.md.
|
||||
"""
|
||||
try:
|
||||
scripts_dir = root / ".trellis" / "scripts"
|
||||
if str(scripts_dir) not in sys.path:
|
||||
sys.path.insert(0, str(scripts_dir))
|
||||
from common.workflow_selection import resolve_workflow_md # type: ignore[import-not-found]
|
||||
|
||||
return resolve_workflow_md(
|
||||
root, input_data, platform=_detect_platform(input_data)
|
||||
)
|
||||
except Exception:
|
||||
return root / ".trellis" / "workflow.md"
|
||||
|
||||
|
||||
def load_breadcrumbs(root: Path, input_data: dict) -> dict[str, str]:
|
||||
"""Parse the resolved workflow file for [workflow-state:STATUS] blocks.
|
||||
|
||||
Returns {status: body_text}. The workflow file 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
|
||||
(or a missing/unreadable workflow file) 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.
|
||||
The active task's per-task workflow selection (task.json `workflow`
|
||||
field) is honored via _resolve_workflow_md; without a selection this
|
||||
reads the global .trellis/workflow.md exactly as before.
|
||||
"""
|
||||
workflow = root / ".trellis" / "workflow.md"
|
||||
workflow = _resolve_workflow_md(root, input_data)
|
||||
if not workflow.is_file():
|
||||
return {}
|
||||
try:
|
||||
@@ -411,7 +447,7 @@ def main() -> int:
|
||||
if prompt_has_skip_keyword(data.get("prompt", ""), _resolve_skip_keyword(config)):
|
||||
return 0 # user opted out of the per-turn breadcrumb for this turn
|
||||
|
||||
templates = load_breadcrumbs(root)
|
||||
templates = load_breadcrumbs(root, data)
|
||||
platform = _detect_platform(data)
|
||||
task = get_active_task(root, data)
|
||||
if task is None:
|
||||
|
||||
Reference in New Issue
Block a user