Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 11 additions & 39 deletions .claude/hooks/inject-workflow-state.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
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 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 the global workflow.md is the single source of
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)
Expand Down Expand Up @@ -141,11 +139,9 @@ def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, st
if not active.task_path:
return None

from common.active_task import resolve_task_ref # type: ignore[import-not-found]

task_dir = resolve_task_ref(active.task_path, root)
if task_dir is None:
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

Expand Down Expand Up @@ -175,40 +171,16 @@ def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, st
re.DOTALL,
)

def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
"""Resolve the active task's workflow file, falling back to the global one.

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.
def load_breadcrumbs(root: Path) -> dict[str, str]:
"""Parse workflow.md for [workflow-state:STATUS] blocks.

Returns {status: body_text}. The workflow file is the single source of
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 file) fall back to a generic line
(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.
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 = _resolve_workflow_md(root, input_data)
workflow = root / ".trellis" / "workflow.md"
if not workflow.is_file():
return {}
try:
Expand Down Expand Up @@ -385,7 +357,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, data)
templates = load_breadcrumbs(root)
platform = _detect_platform(data)
task = get_active_task(root, data)
if task is None:
Expand Down
23 changes: 1 addition & 22 deletions .claude/hooks/session-start.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,27 +699,6 @@ def _strip_breadcrumb_tag_blocks(content: str) -> str:
return re.sub(r"\n{3,}", "\n\n", stripped).strip()


def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
"""Resolve the active task's workflow file, falling back to the global one.

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 _build_workflow_overview(workflow_path: Path) -> str:
"""Inject only the compact Phase Index summary for SessionStart."""
content = read_file(workflow_path)
Expand Down Expand Up @@ -803,7 +782,7 @@ def main():
output.write("\n</current-state>\n\n")

output.write("<trellis-workflow>\n")
output.write(_build_workflow_overview(_resolve_workflow_md(project_dir, hook_input)))
output.write(_build_workflow_overview(trellis_dir / "workflow.md"))
output.write("\n</trellis-workflow>\n\n")

output.write("<guidelines>\n")
Expand Down
44 changes: 9 additions & 35 deletions .codex/hooks/inject-workflow-state.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,9 @@ def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, st
if not active.task_path:
return None

from common.active_task import resolve_task_ref # type: ignore[import-not-found]

task_dir = resolve_task_ref(active.task_path, root)
if task_dir is None:
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

Expand Down Expand Up @@ -173,40 +171,16 @@ def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, st
re.DOTALL,
)

def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
"""Resolve the active task's workflow file, falling back to the global one.

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.
def load_breadcrumbs(root: Path) -> dict[str, str]:
"""Parse workflow.md for [workflow-state:STATUS] blocks.

Returns {status: body_text}. The workflow file is the single source of
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 file) fall back to a generic line
(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.
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 = _resolve_workflow_md(root, input_data)
workflow = root / ".trellis" / "workflow.md"
if not workflow.is_file():
return {}
try:
Expand Down Expand Up @@ -383,7 +357,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, data)
templates = load_breadcrumbs(root)
platform = _detect_platform(data)
task = get_active_task(root, data)
if task is None:
Expand Down
21 changes: 1 addition & 20 deletions .codex/hooks/session-start.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,25 +446,6 @@ def _strip_breadcrumb_tag_blocks(content: str) -> str:
return re.sub(r"\n{3,}", "\n\n", stripped).strip()


def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
"""Resolve the active task's workflow file, falling back to the global one.

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="codex")
except Exception:
return root / ".trellis" / "workflow.md"


def _build_workflow_toc(workflow_path: Path) -> str:
"""Inject only the compact Phase Index summary for SessionStart."""
content = read_file(workflow_path)
Expand Down Expand Up @@ -518,7 +499,7 @@ def main() -> None:
output.write("\n</current-state>\n\n")

output.write("<trellis-workflow>\n")
output.write(_build_workflow_toc(_resolve_workflow_md(project_dir, hook_input)))
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
output.write("\n</trellis-workflow>\n\n")

output.write("<guidelines>\n")
Expand Down
23 changes: 1 addition & 22 deletions .cursor/hooks/session-start.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,27 +699,6 @@ def _strip_breadcrumb_tag_blocks(content: str) -> str:
return re.sub(r"\n{3,}", "\n\n", stripped).strip()


def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
"""Resolve the active task's workflow file, falling back to the global one.

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 _build_workflow_overview(workflow_path: Path) -> str:
"""Inject only the compact Phase Index summary for SessionStart."""
content = read_file(workflow_path)
Expand Down Expand Up @@ -803,7 +782,7 @@ def main():
output.write("\n</current-state>\n\n")

output.write("<trellis-workflow>\n")
output.write(_build_workflow_overview(_resolve_workflow_md(project_dir, hook_input)))
output.write(_build_workflow_overview(trellis_dir / "workflow.md"))
output.write("\n</trellis-workflow>\n\n")

output.write("<guidelines>\n")
Expand Down
Loading
Loading