flower-trellis 0.5.0-beta.2 → 0.5.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/enhancements/0.6/overrides/bundles/intent-routing.json +5 -0
- package/enhancements/0.6/overrides/patches/hooks/claude-session-start/missing-task-routing/content.py +6 -0
- package/enhancements/0.6/overrides/patches/hooks/claude-session-start/missing-task-routing/patch.json +13 -0
- package/enhancements/0.6/overrides/patches/hooks/claude-session-start/missing-task-routing/selector.py +5 -0
- package/enhancements/0.6/overrides/patches/hooks/codex-session-start/missing-task-routing/content.py +6 -0
- package/enhancements/0.6/overrides/patches/hooks/codex-session-start/missing-task-routing/patch.json +13 -0
- package/enhancements/0.6/overrides/patches/hooks/codex-session-start/missing-task-routing/selector.py +4 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/baseline-flower-beta2.py +443 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/baseline-flower-stale-task.py +443 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/content.py +6 -6
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json +9 -2
- package/enhancements/0.6/overrides/patches/workflow/runtime-contract-reference/patch.json +21 -0
- package/enhancements/0.6/overrides/patches/workflow/runtime-contract-reference/runtime-reference-content.md +4 -0
- package/enhancements/0.6/overrides/patches/workflow/runtime-contract-reference/runtime-reference-selector.md +4 -0
- package/enhancements/0.6/overrides/patches/workflow/runtime-contract-reference/state-contract-comment-content.md +46 -0
- package/enhancements/0.6/overrides/patches/workflow/runtime-contract-reference/state-contract-comment-selector.md +44 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/content.md +5 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/patch.json +14 -0
- package/enhancements/0.6/overrides/patches/workflow/state-missing-task/selector.md +1 -0
- package/enhancements/MANIFEST.json +18 -2
- package/package.json +3 -3
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
"workflow/hub",
|
|
8
8
|
"workflow/phase-ownership",
|
|
9
9
|
"workflow/state-no-task",
|
|
10
|
+
"workflow/state-missing-task",
|
|
11
|
+
"workflow/runtime-contract-reference",
|
|
10
12
|
"workflow/states-planning",
|
|
11
13
|
"workflow/states-in-progress",
|
|
12
14
|
"workflow/intent-routing/phase-summary",
|
|
@@ -16,8 +18,11 @@
|
|
|
16
18
|
"workflow/intent-routing/create-task-rule",
|
|
17
19
|
"workflow/intent-routing/create-task-command",
|
|
18
20
|
"workflow/intent-routing/customization-intent-invariant",
|
|
21
|
+
"hooks/inject-workflow-state/shared-runtime",
|
|
19
22
|
"hooks/codex-session-start/no-task-routing",
|
|
23
|
+
"hooks/codex-session-start/missing-task-routing",
|
|
20
24
|
"hooks/claude-session-start/no-task-routing",
|
|
25
|
+
"hooks/claude-session-start/missing-task-routing",
|
|
21
26
|
"skills/trellis-start/no-task-routing",
|
|
22
27
|
"skills/trellis-brainstorm/planning-authorization",
|
|
23
28
|
"skills/trellis-brainstorm/auto-task-create"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
return (
|
|
2
|
+
f"Status: MISSING TASK POINTER\nTask: {task_ref}\n"
|
|
3
|
+
"Next-Action: Run `python3 ./.trellis/scripts/task.py finish`. If cleanup fails, report it "
|
|
4
|
+
"and stop. If cleanup succeeds, treat the current request as NO ACTIVE TASK in the same "
|
|
5
|
+
"turn and follow no_task Request Intent Routing before any edit, task creation, or task start."
|
|
6
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "claude-session-start-missing-task",
|
|
4
|
+
"purpose": "intent_routing",
|
|
5
|
+
"operations": [{
|
|
6
|
+
"id": "claude-session-start-missing-task",
|
|
7
|
+
"operation": "replace",
|
|
8
|
+
"targets": [{ "kind": "hook", "path": ".claude/hooks/session-start.py", "missing": "skip", "markerStyle": "hash" }],
|
|
9
|
+
"selector": { "type": "literal", "source": "selector.py" },
|
|
10
|
+
"content": { "source": "content.py" },
|
|
11
|
+
"legacyMarkers": [{ "namespace": "patch", "id": "claude-session-start-stale-task" }]
|
|
12
|
+
}]
|
|
13
|
+
}
|
package/enhancements/0.6/overrides/patches/hooks/codex-session-start/missing-task-routing/content.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
return (
|
|
2
|
+
f"Status: MISSING TASK POINTER\nTask: {task_ref}\n"
|
|
3
|
+
"Next: Run python3 ./.trellis/scripts/task.py finish. If cleanup fails, report it and stop. "
|
|
4
|
+
"If cleanup succeeds, treat the current request as NO ACTIVE TASK in the same turn and "
|
|
5
|
+
"follow no_task Request Intent Routing before any edit, task creation, or task start."
|
|
6
|
+
)
|
package/enhancements/0.6/overrides/patches/hooks/codex-session-start/missing-task-routing/patch.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "codex-session-start-missing-task",
|
|
4
|
+
"purpose": "intent_routing",
|
|
5
|
+
"operations": [{
|
|
6
|
+
"id": "codex-session-start-missing-task",
|
|
7
|
+
"operation": "replace",
|
|
8
|
+
"targets": [{ "kind": "hook", "path": ".codex/hooks/session-start.py", "missing": "skip", "markerStyle": "hash" }],
|
|
9
|
+
"selector": { "type": "literal", "source": "selector.py" },
|
|
10
|
+
"content": { "source": "content.py" },
|
|
11
|
+
"legacyMarkers": [{ "namespace": "patch", "id": "codex-session-start-stale-task" }]
|
|
12
|
+
}]
|
|
13
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent).
|
|
3
|
+
|
|
4
|
+
Runs on every user prompt. Resolves the active task through Trellis'
|
|
5
|
+
session-aware active task resolver and emits a short <workflow-state>
|
|
6
|
+
block reminding the main AI what task is active and its expected flow.
|
|
7
|
+
|
|
8
|
+
The emitted ``hookEventName`` field is platform-aware: most hosts expect
|
|
9
|
+
``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder /
|
|
10
|
+
CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed
|
|
11
|
+
its per-turn event to ``BeforeAgent`` and its schema validator rejects the
|
|
12
|
+
legacy name. ``_detect_platform`` picks the right value at runtime.
|
|
13
|
+
Breadcrumb text is pulled exclusively from workflow.md
|
|
14
|
+
[workflow-state:STATUS] tag blocks — workflow.md is the single source of
|
|
15
|
+
truth. There are no fallback dicts in this script: when workflow.md is
|
|
16
|
+
missing or a tag is absent, the breadcrumb degrades to a generic
|
|
17
|
+
"Refer to workflow.md for current step." line so users see (and fix)
|
|
18
|
+
the broken state instead of the hook silently masking it.
|
|
19
|
+
|
|
20
|
+
Shared across all hook-capable platforms (Claude, Cursor, Codex, Qoder,
|
|
21
|
+
CodeBuddy, Droid, Gemini, Copilot, Kiro). Kiro wires this via the CLI
|
|
22
|
+
custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook``
|
|
23
|
+
``promptSubmit`` event; its output branch emits a plain-text breadcrumb
|
|
24
|
+
(Kiro adds hook stdout directly to the conversation context). Written to
|
|
25
|
+
each platform's hooks directory via writeSharedHooks() at init time.
|
|
26
|
+
|
|
27
|
+
Silent exit 0 cases (no output):
|
|
28
|
+
- No .trellis/ directory found (not a Trellis project)
|
|
29
|
+
- task.json malformed or missing status
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import queue
|
|
38
|
+
import threading
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is
|
|
42
|
+
# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets)
|
|
43
|
+
# both in stdin (hook payload from host CLI) and stdout (our emitted blocks)
|
|
44
|
+
# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8`
|
|
45
|
+
# but applied per-stream so we don't depend on host CLI's command wiring.
|
|
46
|
+
if sys.platform.startswith("win"):
|
|
47
|
+
import io as _io
|
|
48
|
+
for _stream_name in ("stdin", "stdout", "stderr"):
|
|
49
|
+
_stream = getattr(sys, _stream_name, None)
|
|
50
|
+
if _stream is None:
|
|
51
|
+
continue
|
|
52
|
+
if hasattr(_stream, "reconfigure"):
|
|
53
|
+
try:
|
|
54
|
+
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
elif hasattr(_stream, "detach"):
|
|
58
|
+
try:
|
|
59
|
+
setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"))
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
from typing import Optional
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Bootstrap notice for Codex while the session has no active task. Codex does not
|
|
66
|
+
# get the full SessionStart overview; this short reminder points the main session
|
|
67
|
+
# at the start skill once and leaves the per-turn state block compact.
|
|
68
|
+
CODEX_NO_TASK_BOOTSTRAP_NOTICE = """<trellis-bootstrap>
|
|
69
|
+
If you have not already loaded Trellis context this session, read the `trellis-start` skill once.
|
|
70
|
+
</trellis-bootstrap>"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _codex_has_trellis_session_start(root: Path) -> bool:
|
|
74
|
+
"""判断 Codex 主 SessionStart hook 是否已注册并可执行。
|
|
75
|
+
|
|
76
|
+
`trellis-start` bootstrap 是没有 SessionStart hook 时的兜底。flower-managed
|
|
77
|
+
Codex 项目会补 `.codex/hooks/session-start.py`,因此已注册时不应每轮重复提示。
|
|
78
|
+
读取失败时保守返回 False,继续保留兜底提示。
|
|
79
|
+
"""
|
|
80
|
+
session_start = root / ".codex" / "hooks" / "session-start.py"
|
|
81
|
+
if not session_start.is_file():
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
hooks_path = root / ".codex" / "hooks.json"
|
|
85
|
+
try:
|
|
86
|
+
config = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
87
|
+
except (json.JSONDecodeError, OSError):
|
|
88
|
+
return False
|
|
89
|
+
hooks_config = config.get("hooks")
|
|
90
|
+
if not isinstance(hooks_config, dict):
|
|
91
|
+
return False
|
|
92
|
+
groups = hooks_config.get("SessionStart")
|
|
93
|
+
if not isinstance(groups, list):
|
|
94
|
+
return False
|
|
95
|
+
for group in groups:
|
|
96
|
+
hooks = group.get("hooks") if isinstance(group, dict) else None
|
|
97
|
+
if not isinstance(hooks, list):
|
|
98
|
+
continue
|
|
99
|
+
for hook in hooks:
|
|
100
|
+
if not isinstance(hook, dict):
|
|
101
|
+
continue
|
|
102
|
+
command = hook.get("command")
|
|
103
|
+
if isinstance(command, str) and ".codex/hooks/session-start.py" in command:
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
113
|
+
"""Walk up from start to find directory containing .trellis/.
|
|
114
|
+
|
|
115
|
+
Handles CWD drift: subdirectory launches, monorepo packages, etc.
|
|
116
|
+
Returns None if no .trellis/ found (silent no-op).
|
|
117
|
+
"""
|
|
118
|
+
cur = start.resolve()
|
|
119
|
+
while cur != cur.parent:
|
|
120
|
+
if (cur / ".trellis").is_dir():
|
|
121
|
+
return cur
|
|
122
|
+
cur = cur.parent
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Active task discovery
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def _detect_platform(input_data: dict) -> str | None:
|
|
131
|
+
if isinstance(input_data.get("cursor_version"), str):
|
|
132
|
+
return "cursor"
|
|
133
|
+
env_map = {
|
|
134
|
+
"CLAUDE_PROJECT_DIR": "claude",
|
|
135
|
+
"CURSOR_PROJECT_DIR": "cursor",
|
|
136
|
+
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
|
137
|
+
"FACTORY_PROJECT_DIR": "droid",
|
|
138
|
+
"GEMINI_PROJECT_DIR": "gemini",
|
|
139
|
+
"QODER_PROJECT_DIR": "qoder",
|
|
140
|
+
"KIRO_PROJECT_DIR": "kiro",
|
|
141
|
+
"COPILOT_PROJECT_DIR": "copilot",
|
|
142
|
+
"TRAE_PROJECT_DIR": "trae",
|
|
143
|
+
}
|
|
144
|
+
for env_name, platform in env_map.items():
|
|
145
|
+
if os.environ.get(env_name):
|
|
146
|
+
return platform
|
|
147
|
+
script_parts = set(Path(sys.argv[0]).parts)
|
|
148
|
+
if ".claude" in script_parts:
|
|
149
|
+
return "claude"
|
|
150
|
+
if ".cursor" in script_parts:
|
|
151
|
+
return "cursor"
|
|
152
|
+
if ".codex" in script_parts:
|
|
153
|
+
return "codex"
|
|
154
|
+
if ".gemini" in script_parts:
|
|
155
|
+
return "gemini"
|
|
156
|
+
if ".qoder" in script_parts:
|
|
157
|
+
return "qoder"
|
|
158
|
+
if ".codebuddy" in script_parts:
|
|
159
|
+
return "codebuddy"
|
|
160
|
+
if ".factory" in script_parts:
|
|
161
|
+
return "droid"
|
|
162
|
+
if ".kiro" in script_parts:
|
|
163
|
+
return "kiro"
|
|
164
|
+
if ".trae" in script_parts:
|
|
165
|
+
return "trae"
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _resolve_active_task(root: Path, input_data: dict):
|
|
170
|
+
scripts_dir = root / ".trellis" / "scripts"
|
|
171
|
+
if str(scripts_dir) not in sys.path:
|
|
172
|
+
sys.path.insert(0, str(scripts_dir))
|
|
173
|
+
from common.active_task import resolve_active_task # type: ignore[import-not-found]
|
|
174
|
+
|
|
175
|
+
return resolve_active_task(root, input_data, platform=_detect_platform(input_data))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]:
|
|
179
|
+
"""Return (task_id, status, source) from the current active task."""
|
|
180
|
+
active = _resolve_active_task(root, input_data)
|
|
181
|
+
if not active.task_path:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
task_dir = Path(active.task_path)
|
|
185
|
+
if not task_dir.is_absolute():
|
|
186
|
+
task_dir = root / task_dir
|
|
187
|
+
if active.stale:
|
|
188
|
+
return task_dir.name, f"stale_{active.source_type}", active.source
|
|
189
|
+
|
|
190
|
+
task_json = task_dir / "task.json"
|
|
191
|
+
if not task_json.is_file():
|
|
192
|
+
return None
|
|
193
|
+
try:
|
|
194
|
+
data = json.loads(task_json.read_text(encoding="utf-8"))
|
|
195
|
+
except (json.JSONDecodeError, OSError):
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
task_id = data.get("id") or task_dir.name
|
|
199
|
+
status = data.get("status", "")
|
|
200
|
+
if not isinstance(status, str) or not status:
|
|
201
|
+
return None
|
|
202
|
+
return task_id, status, active.source
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
# Supports STATUS values with letters, digits, underscores, hyphens
|
|
210
|
+
# (so "in-review" / "blocked-by-team" work alongside "in_progress").
|
|
211
|
+
_TAG_RE = re.compile(
|
|
212
|
+
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]",
|
|
213
|
+
re.DOTALL,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def load_breadcrumbs(root: Path) -> dict[str, str]:
|
|
217
|
+
"""Parse workflow.md for [workflow-state:STATUS] blocks.
|
|
218
|
+
|
|
219
|
+
Returns {status: body_text}. workflow.md is the single source of
|
|
220
|
+
truth — there are no fallback dicts in this script. Missing tags
|
|
221
|
+
(or a missing/unreadable workflow.md) fall back to a generic line
|
|
222
|
+
in build_breadcrumb so users see the broken state and fix
|
|
223
|
+
workflow.md, rather than the hook silently masking the issue.
|
|
224
|
+
"""
|
|
225
|
+
workflow = root / ".trellis" / "workflow.md"
|
|
226
|
+
if not workflow.is_file():
|
|
227
|
+
return {}
|
|
228
|
+
try:
|
|
229
|
+
content = workflow.read_text(encoding="utf-8")
|
|
230
|
+
except OSError:
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
result: dict[str, str] = {}
|
|
234
|
+
for match in _TAG_RE.finditer(content):
|
|
235
|
+
status = match.group(1)
|
|
236
|
+
body = match.group(2).strip()
|
|
237
|
+
if body:
|
|
238
|
+
result[status] = body
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _read_trellis_config(root: Path) -> dict:
|
|
243
|
+
"""Load .trellis/config.yaml via the bundled trellis_config helper.
|
|
244
|
+
|
|
245
|
+
The helper lives in .trellis/scripts/common; the hook lives outside the
|
|
246
|
+
scripts tree, so we extend sys.path before importing.
|
|
247
|
+
"""
|
|
248
|
+
scripts_dir = root / ".trellis" / "scripts"
|
|
249
|
+
if str(scripts_dir) not in sys.path:
|
|
250
|
+
sys.path.insert(0, str(scripts_dir))
|
|
251
|
+
try:
|
|
252
|
+
from common.trellis_config import read_trellis_config # type: ignore[import-not-found]
|
|
253
|
+
except Exception:
|
|
254
|
+
return {}
|
|
255
|
+
try:
|
|
256
|
+
return read_trellis_config(root)
|
|
257
|
+
except Exception:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _codex_mode_banner(config: dict) -> str:
|
|
262
|
+
"""Emit a `<codex-mode>` banner for the additionalContext payload.
|
|
263
|
+
|
|
264
|
+
Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to
|
|
265
|
+
`inline` when missing or invalid because Codex sub-agents run with
|
|
266
|
+
`fork_turns="none"` isolation and can't inherit the parent session's
|
|
267
|
+
task context. The banner makes the active mode explicit to Codex AI
|
|
268
|
+
per turn, complementing the workflow-state body which is per-status.
|
|
269
|
+
Mode tells AI which dispatch protocol to follow; workflow-state tells
|
|
270
|
+
AI what step it's at.
|
|
271
|
+
"""
|
|
272
|
+
mode = "inline"
|
|
273
|
+
if isinstance(config, dict):
|
|
274
|
+
codex_cfg = config.get("codex")
|
|
275
|
+
if isinstance(codex_cfg, dict):
|
|
276
|
+
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
277
|
+
if cfg_mode in ("inline", "sub-agent"):
|
|
278
|
+
mode = cfg_mode
|
|
279
|
+
if mode == "sub-agent":
|
|
280
|
+
meaning = (
|
|
281
|
+
"sub-agent: implement/check work defaults to Trellis sub-agents; "
|
|
282
|
+
"the main session still coordinates, clarifies, updates specs, commits, and finishes."
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
meaning = (
|
|
286
|
+
"inline: the main session implements/checks directly; "
|
|
287
|
+
"do not dispatch implement/check sub-agents."
|
|
288
|
+
)
|
|
289
|
+
return f"<codex-mode>{meaning}</codex-mode>"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def resolve_breadcrumb_key(
|
|
293
|
+
status: str, platform: str | None, config: dict
|
|
294
|
+
) -> str:
|
|
295
|
+
"""Pick the breadcrumb tag key based on Codex dispatch_mode.
|
|
296
|
+
|
|
297
|
+
Codex defaults to ``inline`` because sub-agents run with ``fork_turns="none"``
|
|
298
|
+
isolation and can't inherit the parent session's task context. Users can
|
|
299
|
+
opt into ``codex.dispatch_mode: sub-agent`` in ``.trellis/config.yaml``
|
|
300
|
+
to use the parallel ``<status>-inline`` tag → ``<status>`` flip. Invalid
|
|
301
|
+
or missing values fall back to inline.
|
|
302
|
+
|
|
303
|
+
Non-codex platforms return the plain status unchanged.
|
|
304
|
+
"""
|
|
305
|
+
if platform == "codex":
|
|
306
|
+
mode = "inline"
|
|
307
|
+
if isinstance(config, dict):
|
|
308
|
+
codex_cfg = config.get("codex")
|
|
309
|
+
if isinstance(codex_cfg, dict):
|
|
310
|
+
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
311
|
+
if cfg_mode in ("inline", "sub-agent"):
|
|
312
|
+
mode = cfg_mode
|
|
313
|
+
return f"{status}-inline" if mode == "inline" else status
|
|
314
|
+
return status
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def build_breadcrumb(
|
|
318
|
+
task_id: Optional[str],
|
|
319
|
+
status: str,
|
|
320
|
+
templates: dict[str, str],
|
|
321
|
+
source: str | None = None,
|
|
322
|
+
breadcrumb_key: str | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Build the <workflow-state>...</workflow-state> block.
|
|
325
|
+
|
|
326
|
+
- Known status (tag present in workflow.md) → detailed template body
|
|
327
|
+
- Unknown status (no tag, or workflow.md missing) → generic
|
|
328
|
+
"Refer to workflow.md for current step." line
|
|
329
|
+
- `no_task` pseudo-status (task_id is None) → header omits task info
|
|
330
|
+
"""
|
|
331
|
+
lookup_key = breadcrumb_key or status
|
|
332
|
+
body = templates.get(lookup_key)
|
|
333
|
+
if body is None and lookup_key != status:
|
|
334
|
+
body = templates.get(status)
|
|
335
|
+
if body is None:
|
|
336
|
+
body = "Refer to workflow.md for current step."
|
|
337
|
+
header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"
|
|
338
|
+
return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Entry
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
def _load_hook_input() -> dict:
|
|
346
|
+
"""Read hook JSON without trusting host runners to close stdin.
|
|
347
|
+
|
|
348
|
+
Kiro IDE `runCommand` and similar hook runners can leave stdin open while
|
|
349
|
+
sending no payload. A plain `json.load(sys.stdin)` then blocks forever.
|
|
350
|
+
Normal hook runners write the complete JSON payload and close stdin, so the
|
|
351
|
+
short daemon read preserves that path while failing closed to `{}` for
|
|
352
|
+
non-piping hosts.
|
|
353
|
+
"""
|
|
354
|
+
result_queue: "queue.Queue[str | BaseException]" = queue.Queue(maxsize=1)
|
|
355
|
+
|
|
356
|
+
def _read() -> None:
|
|
357
|
+
try:
|
|
358
|
+
result_queue.put(sys.stdin.read())
|
|
359
|
+
except BaseException as exc:
|
|
360
|
+
result_queue.put(exc)
|
|
361
|
+
|
|
362
|
+
reader = threading.Thread(target=_read, daemon=True)
|
|
363
|
+
reader.start()
|
|
364
|
+
try:
|
|
365
|
+
raw = result_queue.get(timeout=0.2)
|
|
366
|
+
except queue.Empty:
|
|
367
|
+
return {}
|
|
368
|
+
|
|
369
|
+
if isinstance(raw, BaseException):
|
|
370
|
+
return {}
|
|
371
|
+
try:
|
|
372
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
373
|
+
except (json.JSONDecodeError, ValueError):
|
|
374
|
+
return {}
|
|
375
|
+
return data if isinstance(data, dict) else {}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def main() -> int:
|
|
379
|
+
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
data = _load_hook_input()
|
|
383
|
+
|
|
384
|
+
cwd_str = data.get("cwd") or os.getcwd()
|
|
385
|
+
cwd = Path(cwd_str)
|
|
386
|
+
|
|
387
|
+
root = find_trellis_root(cwd)
|
|
388
|
+
if root is None:
|
|
389
|
+
return 0 # not a Trellis project
|
|
390
|
+
|
|
391
|
+
templates = load_breadcrumbs(root)
|
|
392
|
+
platform = _detect_platform(data)
|
|
393
|
+
config = _read_trellis_config(root)
|
|
394
|
+
task = get_active_task(root, data)
|
|
395
|
+
if task is None:
|
|
396
|
+
# No active task — still emit a breadcrumb nudging AI toward
|
|
397
|
+
# trellis-brainstorm + task.py create when user describes real work.
|
|
398
|
+
no_task_key = resolve_breadcrumb_key("no_task", platform, config)
|
|
399
|
+
breadcrumb = build_breadcrumb(
|
|
400
|
+
None, "no_task", templates, breadcrumb_key=no_task_key
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
task_id, status, source = task
|
|
404
|
+
status_key = resolve_breadcrumb_key(status, platform, config)
|
|
405
|
+
source_for_breadcrumb = None if platform == "codex" else source
|
|
406
|
+
breadcrumb = build_breadcrumb(
|
|
407
|
+
task_id, status, templates, source_for_breadcrumb, breadcrumb_key=status_key
|
|
408
|
+
)
|
|
409
|
+
if platform == "codex":
|
|
410
|
+
parts: list[str] = []
|
|
411
|
+
if task is None and not _codex_has_trellis_session_start(root):
|
|
412
|
+
parts.append(CODEX_NO_TASK_BOOTSTRAP_NOTICE)
|
|
413
|
+
parts.append(_codex_mode_banner(config))
|
|
414
|
+
parts.append(breadcrumb)
|
|
415
|
+
breadcrumb = "\n\n".join(parts)
|
|
416
|
+
|
|
417
|
+
# Kiro (CLI userPromptSubmit / IDE promptSubmit) adds a hook's stdout
|
|
418
|
+
# directly to the conversation context — no JSON envelope. Emit the bare
|
|
419
|
+
# breadcrumb text. Conditionally isolated: all other platforms keep the
|
|
420
|
+
# hookSpecificOutput JSON path below unchanged.
|
|
421
|
+
if platform == "kiro":
|
|
422
|
+
print(breadcrumb)
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
# Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is
|
|
426
|
+
# named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/
|
|
427
|
+
# Droid/Codex/Copilot) accept the original Claude-style name.
|
|
428
|
+
hook_event_name = (
|
|
429
|
+
"BeforeAgent" if platform == "gemini" else "UserPromptSubmit"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
output = {
|
|
433
|
+
"hookSpecificOutput": {
|
|
434
|
+
"hookEventName": hook_event_name,
|
|
435
|
+
"additionalContext": breadcrumb,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
print(json.dumps(output))
|
|
439
|
+
return 0
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
sys.exit(main())
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Trellis per-turn breadcrumb hook (UserPromptSubmit / BeforeAgent equivalent).
|
|
3
|
+
|
|
4
|
+
Runs on every user prompt. Resolves the active task through Trellis'
|
|
5
|
+
session-aware active task resolver and emits a short <workflow-state>
|
|
6
|
+
block reminding the main AI what task is active and its expected flow.
|
|
7
|
+
|
|
8
|
+
The emitted ``hookEventName`` field is platform-aware: most hosts expect
|
|
9
|
+
``UserPromptSubmit`` (Claude Code naming, also accepted by Cursor / Qoder /
|
|
10
|
+
CodeBuddy / Droid / Codex / Copilot wiring), but Gemini CLI 0.40.x renamed
|
|
11
|
+
its per-turn event to ``BeforeAgent`` and its schema validator rejects the
|
|
12
|
+
legacy name. ``_detect_platform`` picks the right value at runtime.
|
|
13
|
+
Breadcrumb text is pulled exclusively from workflow.md
|
|
14
|
+
[workflow-state:STATUS] tag blocks — workflow.md is the single source of
|
|
15
|
+
truth. There are no fallback dicts in this script: when workflow.md is
|
|
16
|
+
missing or a tag is absent, the breadcrumb degrades to a generic
|
|
17
|
+
"Refer to workflow.md for current step." line so users see (and fix)
|
|
18
|
+
the broken state instead of the hook silently masking it.
|
|
19
|
+
|
|
20
|
+
Shared across platforms with a per-turn workflow-state hook (Claude, Codex,
|
|
21
|
+
Gemini, Qoder, Copilot, CodeBuddy, Droid, Kiro, Trae). Kiro wires this via the
|
|
22
|
+
CLI custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook``
|
|
23
|
+
``promptSubmit`` event; its output branch emits a plain-text breadcrumb
|
|
24
|
+
(Kiro adds hook stdout directly to the conversation context). Written to each
|
|
25
|
+
platform's hooks directory via writeSharedHooks() at init time.
|
|
26
|
+
|
|
27
|
+
Silent exit 0 cases (no output):
|
|
28
|
+
- No .trellis/ directory found (not a Trellis project)
|
|
29
|
+
- task.json malformed or missing status
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import queue
|
|
38
|
+
import threading
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is
|
|
42
|
+
# cp936 / cp1252 / etc. — non-ASCII content (Chinese task names, prd snippets)
|
|
43
|
+
# both in stdin (hook payload from host CLI) and stdout (our emitted blocks)
|
|
44
|
+
# raises UnicodeDecodeError / UnicodeEncodeError. Equivalent to `python -X utf8`
|
|
45
|
+
# but applied per-stream so we don't depend on host CLI's command wiring.
|
|
46
|
+
if sys.platform.startswith("win"):
|
|
47
|
+
import io as _io
|
|
48
|
+
for _stream_name in ("stdin", "stdout", "stderr"):
|
|
49
|
+
_stream = getattr(sys, _stream_name, None)
|
|
50
|
+
if _stream is None:
|
|
51
|
+
continue
|
|
52
|
+
if hasattr(_stream, "reconfigure"):
|
|
53
|
+
try:
|
|
54
|
+
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
elif hasattr(_stream, "detach"):
|
|
58
|
+
try:
|
|
59
|
+
setattr(sys, _stream_name, _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"))
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
from typing import Optional
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Bootstrap notice for Codex while the session has no active task. Codex does not
|
|
66
|
+
# get the full SessionStart overview; this short reminder points the main session
|
|
67
|
+
# at the start skill once and leaves the per-turn state block compact.
|
|
68
|
+
CODEX_NO_TASK_BOOTSTRAP_NOTICE = """<trellis-bootstrap>
|
|
69
|
+
If you have not already loaded Trellis context this session, read the `trellis-start` skill once.
|
|
70
|
+
</trellis-bootstrap>"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _codex_has_trellis_session_start(root: Path) -> bool:
|
|
74
|
+
"""判断 Codex 主 SessionStart hook 是否已注册并可执行。
|
|
75
|
+
|
|
76
|
+
`trellis-start` bootstrap 是没有 SessionStart hook 时的兜底。flower-managed
|
|
77
|
+
Codex 项目会补 `.codex/hooks/session-start.py`,因此已注册时不应每轮重复提示。
|
|
78
|
+
读取失败时保守返回 False,继续保留兜底提示。
|
|
79
|
+
"""
|
|
80
|
+
session_start = root / ".codex" / "hooks" / "session-start.py"
|
|
81
|
+
if not session_start.is_file():
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
hooks_path = root / ".codex" / "hooks.json"
|
|
85
|
+
try:
|
|
86
|
+
config = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
87
|
+
except (json.JSONDecodeError, OSError):
|
|
88
|
+
return False
|
|
89
|
+
hooks_config = config.get("hooks")
|
|
90
|
+
if not isinstance(hooks_config, dict):
|
|
91
|
+
return False
|
|
92
|
+
groups = hooks_config.get("SessionStart")
|
|
93
|
+
if not isinstance(groups, list):
|
|
94
|
+
return False
|
|
95
|
+
for group in groups:
|
|
96
|
+
hooks = group.get("hooks") if isinstance(group, dict) else None
|
|
97
|
+
if not isinstance(hooks, list):
|
|
98
|
+
continue
|
|
99
|
+
for hook in hooks:
|
|
100
|
+
if not isinstance(hook, dict):
|
|
101
|
+
continue
|
|
102
|
+
command = hook.get("command")
|
|
103
|
+
if isinstance(command, str) and ".codex/hooks/session-start.py" in command:
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# CWD-robust Trellis root discovery (fixes hook-path-robustness for this hook)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
113
|
+
"""Walk up from start to find directory containing .trellis/.
|
|
114
|
+
|
|
115
|
+
Handles CWD drift: subdirectory launches, monorepo packages, etc.
|
|
116
|
+
Returns None if no .trellis/ found (silent no-op).
|
|
117
|
+
"""
|
|
118
|
+
cur = start.resolve()
|
|
119
|
+
while cur != cur.parent:
|
|
120
|
+
if (cur / ".trellis").is_dir():
|
|
121
|
+
return cur
|
|
122
|
+
cur = cur.parent
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# Active task discovery
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def _detect_platform(input_data: dict) -> str | None:
|
|
131
|
+
if isinstance(input_data.get("cursor_version"), str):
|
|
132
|
+
return "cursor"
|
|
133
|
+
env_map = {
|
|
134
|
+
"CLAUDE_PROJECT_DIR": "claude",
|
|
135
|
+
"CURSOR_PROJECT_DIR": "cursor",
|
|
136
|
+
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
|
137
|
+
"FACTORY_PROJECT_DIR": "droid",
|
|
138
|
+
"GEMINI_PROJECT_DIR": "gemini",
|
|
139
|
+
"QODER_PROJECT_DIR": "qoder",
|
|
140
|
+
"KIRO_PROJECT_DIR": "kiro",
|
|
141
|
+
"COPILOT_PROJECT_DIR": "copilot",
|
|
142
|
+
"TRAE_PROJECT_DIR": "trae",
|
|
143
|
+
}
|
|
144
|
+
for env_name, platform in env_map.items():
|
|
145
|
+
if os.environ.get(env_name):
|
|
146
|
+
return platform
|
|
147
|
+
script_parts = set(Path(sys.argv[0]).parts)
|
|
148
|
+
if ".claude" in script_parts:
|
|
149
|
+
return "claude"
|
|
150
|
+
if ".cursor" in script_parts:
|
|
151
|
+
return "cursor"
|
|
152
|
+
if ".codex" in script_parts:
|
|
153
|
+
return "codex"
|
|
154
|
+
if ".gemini" in script_parts:
|
|
155
|
+
return "gemini"
|
|
156
|
+
if ".qoder" in script_parts:
|
|
157
|
+
return "qoder"
|
|
158
|
+
if ".codebuddy" in script_parts:
|
|
159
|
+
return "codebuddy"
|
|
160
|
+
if ".factory" in script_parts:
|
|
161
|
+
return "droid"
|
|
162
|
+
if ".kiro" in script_parts:
|
|
163
|
+
return "kiro"
|
|
164
|
+
if ".trae" in script_parts:
|
|
165
|
+
return "trae"
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _resolve_active_task(root: Path, input_data: dict):
|
|
170
|
+
scripts_dir = root / ".trellis" / "scripts"
|
|
171
|
+
if str(scripts_dir) not in sys.path:
|
|
172
|
+
sys.path.insert(0, str(scripts_dir))
|
|
173
|
+
from common.active_task import resolve_active_task # type: ignore[import-not-found]
|
|
174
|
+
|
|
175
|
+
return resolve_active_task(root, input_data, platform=_detect_platform(input_data))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, str]]:
|
|
179
|
+
"""Return (task_id, status, source) from the current active task."""
|
|
180
|
+
active = _resolve_active_task(root, input_data)
|
|
181
|
+
if not active.task_path:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
task_dir = Path(active.task_path)
|
|
185
|
+
if not task_dir.is_absolute():
|
|
186
|
+
task_dir = root / task_dir
|
|
187
|
+
if active.stale:
|
|
188
|
+
return task_dir.name, "stale_task", active.source
|
|
189
|
+
|
|
190
|
+
task_json = task_dir / "task.json"
|
|
191
|
+
if not task_json.is_file():
|
|
192
|
+
return None
|
|
193
|
+
try:
|
|
194
|
+
data = json.loads(task_json.read_text(encoding="utf-8"))
|
|
195
|
+
except (json.JSONDecodeError, OSError):
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
task_id = data.get("id") or task_dir.name
|
|
199
|
+
status = data.get("status", "")
|
|
200
|
+
if not isinstance(status, str) or not status:
|
|
201
|
+
return None
|
|
202
|
+
return task_id, status, active.source
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Breadcrumb loading: parse workflow.md, fall back to hardcoded defaults
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
# Supports STATUS values with letters, digits, underscores, hyphens
|
|
210
|
+
# (so "in-review" / "blocked-by-team" work alongside "in_progress").
|
|
211
|
+
_TAG_RE = re.compile(
|
|
212
|
+
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n(.*?)\n\s*\[/workflow-state:\1\]",
|
|
213
|
+
re.DOTALL,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def load_breadcrumbs(root: Path) -> dict[str, str]:
|
|
217
|
+
"""Parse workflow.md for [workflow-state:STATUS] blocks.
|
|
218
|
+
|
|
219
|
+
Returns {status: body_text}. workflow.md is the single source of
|
|
220
|
+
truth — there are no fallback dicts in this script. Missing tags
|
|
221
|
+
(or a missing/unreadable workflow.md) fall back to a generic line
|
|
222
|
+
in build_breadcrumb so users see the broken state and fix
|
|
223
|
+
workflow.md, rather than the hook silently masking the issue.
|
|
224
|
+
"""
|
|
225
|
+
workflow = root / ".trellis" / "workflow.md"
|
|
226
|
+
if not workflow.is_file():
|
|
227
|
+
return {}
|
|
228
|
+
try:
|
|
229
|
+
content = workflow.read_text(encoding="utf-8")
|
|
230
|
+
except OSError:
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
result: dict[str, str] = {}
|
|
234
|
+
for match in _TAG_RE.finditer(content):
|
|
235
|
+
status = match.group(1)
|
|
236
|
+
body = match.group(2).strip()
|
|
237
|
+
if body:
|
|
238
|
+
result[status] = body
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _read_trellis_config(root: Path) -> dict:
|
|
243
|
+
"""Load .trellis/config.yaml via the bundled trellis_config helper.
|
|
244
|
+
|
|
245
|
+
The helper lives in .trellis/scripts/common; the hook lives outside the
|
|
246
|
+
scripts tree, so we extend sys.path before importing.
|
|
247
|
+
"""
|
|
248
|
+
scripts_dir = root / ".trellis" / "scripts"
|
|
249
|
+
if str(scripts_dir) not in sys.path:
|
|
250
|
+
sys.path.insert(0, str(scripts_dir))
|
|
251
|
+
try:
|
|
252
|
+
from common.trellis_config import read_trellis_config # type: ignore[import-not-found]
|
|
253
|
+
except Exception:
|
|
254
|
+
return {}
|
|
255
|
+
try:
|
|
256
|
+
return read_trellis_config(root)
|
|
257
|
+
except Exception:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _codex_mode_banner(config: dict) -> str:
|
|
262
|
+
"""Emit a `<codex-mode>` banner for the additionalContext payload.
|
|
263
|
+
|
|
264
|
+
Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to
|
|
265
|
+
`inline` when missing or invalid because Codex sub-agents run with
|
|
266
|
+
`fork_turns="none"` isolation and can't inherit the parent session's
|
|
267
|
+
task context. The banner makes the active mode explicit to Codex AI
|
|
268
|
+
per turn, complementing the workflow-state body which is per-status.
|
|
269
|
+
Mode tells AI which dispatch protocol to follow; workflow-state tells
|
|
270
|
+
AI what step it's at.
|
|
271
|
+
"""
|
|
272
|
+
mode = "inline"
|
|
273
|
+
if isinstance(config, dict):
|
|
274
|
+
codex_cfg = config.get("codex")
|
|
275
|
+
if isinstance(codex_cfg, dict):
|
|
276
|
+
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
277
|
+
if cfg_mode in ("inline", "sub-agent"):
|
|
278
|
+
mode = cfg_mode
|
|
279
|
+
if mode == "sub-agent":
|
|
280
|
+
meaning = (
|
|
281
|
+
"sub-agent: implement/check work defaults to Trellis sub-agents; "
|
|
282
|
+
"the main session still coordinates, clarifies, updates specs, commits, and finishes."
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
meaning = (
|
|
286
|
+
"inline: the main session implements/checks directly; "
|
|
287
|
+
"do not dispatch implement/check sub-agents."
|
|
288
|
+
)
|
|
289
|
+
return f"<codex-mode>{meaning}</codex-mode>"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def resolve_breadcrumb_key(
|
|
293
|
+
status: str, platform: str | None, config: dict
|
|
294
|
+
) -> str:
|
|
295
|
+
"""Pick the breadcrumb tag key based on Codex dispatch_mode.
|
|
296
|
+
|
|
297
|
+
Codex defaults to ``inline`` because sub-agents run with ``fork_turns="none"``
|
|
298
|
+
isolation and can't inherit the parent session's task context. Users can
|
|
299
|
+
opt into ``codex.dispatch_mode: sub-agent`` in ``.trellis/config.yaml``
|
|
300
|
+
to use the parallel ``<status>-inline`` tag → ``<status>`` flip. Invalid
|
|
301
|
+
or missing values fall back to inline.
|
|
302
|
+
|
|
303
|
+
Non-codex platforms return the plain status unchanged.
|
|
304
|
+
"""
|
|
305
|
+
if platform == "codex":
|
|
306
|
+
mode = "inline"
|
|
307
|
+
if isinstance(config, dict):
|
|
308
|
+
codex_cfg = config.get("codex")
|
|
309
|
+
if isinstance(codex_cfg, dict):
|
|
310
|
+
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
311
|
+
if cfg_mode in ("inline", "sub-agent"):
|
|
312
|
+
mode = cfg_mode
|
|
313
|
+
return f"{status}-inline" if mode == "inline" else status
|
|
314
|
+
return status
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def build_breadcrumb(
|
|
318
|
+
task_id: Optional[str],
|
|
319
|
+
status: str,
|
|
320
|
+
templates: dict[str, str],
|
|
321
|
+
source: str | None = None,
|
|
322
|
+
breadcrumb_key: str | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Build the <workflow-state>...</workflow-state> block.
|
|
325
|
+
|
|
326
|
+
- Known status (tag present in workflow.md) → detailed template body
|
|
327
|
+
- Unknown status (no tag, or workflow.md missing) → generic
|
|
328
|
+
"Refer to workflow.md for current step." line
|
|
329
|
+
- `no_task` pseudo-status (task_id is None) → header omits task info
|
|
330
|
+
"""
|
|
331
|
+
lookup_key = breadcrumb_key or status
|
|
332
|
+
body = templates.get(lookup_key)
|
|
333
|
+
if body is None and lookup_key != status:
|
|
334
|
+
body = templates.get(status)
|
|
335
|
+
if body is None:
|
|
336
|
+
body = "Refer to workflow.md for current step."
|
|
337
|
+
header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"
|
|
338
|
+
return f"<workflow-state>\n{header}\n{body}\n</workflow-state>"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Entry
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
def _load_hook_input() -> dict:
|
|
346
|
+
"""Read hook JSON without trusting host runners to close stdin.
|
|
347
|
+
|
|
348
|
+
Kiro IDE `runCommand` and similar hook runners can leave stdin open while
|
|
349
|
+
sending no payload. A plain `json.load(sys.stdin)` then blocks forever.
|
|
350
|
+
Normal hook runners write the complete JSON payload and close stdin, so the
|
|
351
|
+
short daemon read preserves that path while failing closed to `{}` for
|
|
352
|
+
non-piping hosts.
|
|
353
|
+
"""
|
|
354
|
+
result_queue: "queue.Queue[str | BaseException]" = queue.Queue(maxsize=1)
|
|
355
|
+
|
|
356
|
+
def _read() -> None:
|
|
357
|
+
try:
|
|
358
|
+
result_queue.put(sys.stdin.read())
|
|
359
|
+
except BaseException as exc:
|
|
360
|
+
result_queue.put(exc)
|
|
361
|
+
|
|
362
|
+
reader = threading.Thread(target=_read, daemon=True)
|
|
363
|
+
reader.start()
|
|
364
|
+
try:
|
|
365
|
+
raw = result_queue.get(timeout=0.2)
|
|
366
|
+
except queue.Empty:
|
|
367
|
+
return {}
|
|
368
|
+
|
|
369
|
+
if isinstance(raw, BaseException):
|
|
370
|
+
return {}
|
|
371
|
+
try:
|
|
372
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
373
|
+
except (json.JSONDecodeError, ValueError):
|
|
374
|
+
return {}
|
|
375
|
+
return data if isinstance(data, dict) else {}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def main() -> int:
|
|
379
|
+
if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
data = _load_hook_input()
|
|
383
|
+
|
|
384
|
+
cwd_str = data.get("cwd") or os.getcwd()
|
|
385
|
+
cwd = Path(cwd_str)
|
|
386
|
+
|
|
387
|
+
root = find_trellis_root(cwd)
|
|
388
|
+
if root is None:
|
|
389
|
+
return 0 # not a Trellis project
|
|
390
|
+
|
|
391
|
+
templates = load_breadcrumbs(root)
|
|
392
|
+
platform = _detect_platform(data)
|
|
393
|
+
config = _read_trellis_config(root)
|
|
394
|
+
task = get_active_task(root, data)
|
|
395
|
+
if task is None:
|
|
396
|
+
# No active task — still emit a breadcrumb nudging AI toward
|
|
397
|
+
# trellis-brainstorm + task.py create when user describes real work.
|
|
398
|
+
no_task_key = resolve_breadcrumb_key("no_task", platform, config)
|
|
399
|
+
breadcrumb = build_breadcrumb(
|
|
400
|
+
None, "no_task", templates, breadcrumb_key=no_task_key
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
task_id, status, source = task
|
|
404
|
+
status_key = resolve_breadcrumb_key(status, platform, config)
|
|
405
|
+
source_for_breadcrumb = None if platform == "codex" else source
|
|
406
|
+
breadcrumb = build_breadcrumb(
|
|
407
|
+
task_id, status, templates, source_for_breadcrumb, breadcrumb_key=status_key
|
|
408
|
+
)
|
|
409
|
+
if platform == "codex":
|
|
410
|
+
parts: list[str] = []
|
|
411
|
+
if task is None and not _codex_has_trellis_session_start(root):
|
|
412
|
+
parts.append(CODEX_NO_TASK_BOOTSTRAP_NOTICE)
|
|
413
|
+
parts.append(_codex_mode_banner(config))
|
|
414
|
+
parts.append(breadcrumb)
|
|
415
|
+
breadcrumb = "\n\n".join(parts)
|
|
416
|
+
|
|
417
|
+
# Kiro (CLI userPromptSubmit / IDE promptSubmit) adds a hook's stdout
|
|
418
|
+
# directly to the conversation context — no JSON envelope. Emit the bare
|
|
419
|
+
# breadcrumb text. Conditionally isolated: all other platforms keep the
|
|
420
|
+
# hookSpecificOutput JSON path below unchanged.
|
|
421
|
+
if platform == "kiro":
|
|
422
|
+
print(breadcrumb)
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
# Gemini CLI 0.40.x rejects "UserPromptSubmit" — its per-turn event is
|
|
426
|
+
# named "BeforeAgent". Other platforms (Claude/Cursor/Qoder/CodeBuddy/
|
|
427
|
+
# Droid/Codex/Copilot) accept the original Claude-style name.
|
|
428
|
+
hook_event_name = (
|
|
429
|
+
"BeforeAgent" if platform == "gemini" else "UserPromptSubmit"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
output = {
|
|
433
|
+
"hookSpecificOutput": {
|
|
434
|
+
"hookEventName": hook_event_name,
|
|
435
|
+
"additionalContext": breadcrumb,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
print(json.dumps(output))
|
|
439
|
+
return 0
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
sys.exit(main())
|
package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/content.py
CHANGED
|
@@ -17,12 +17,12 @@ missing or a tag is absent, the breadcrumb degrades to a generic
|
|
|
17
17
|
"Refer to workflow.md for current step." line so users see (and fix)
|
|
18
18
|
the broken state instead of the hook silently masking it.
|
|
19
19
|
|
|
20
|
-
Shared across
|
|
21
|
-
|
|
22
|
-
custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook``
|
|
20
|
+
Shared across platforms with a per-turn workflow-state hook (Claude, Codex,
|
|
21
|
+
Gemini, Qoder, Copilot, CodeBuddy, Droid, Kiro, Trae). Kiro wires this via the
|
|
22
|
+
CLI custom agent's ``hooks.userPromptSubmit`` and the IDE ``.kiro.hook``
|
|
23
23
|
``promptSubmit`` event; its output branch emits a plain-text breadcrumb
|
|
24
|
-
(Kiro adds hook stdout directly to the conversation context). Written to
|
|
25
|
-
|
|
24
|
+
(Kiro adds hook stdout directly to the conversation context). Written to each
|
|
25
|
+
platform's hooks directory via writeSharedHooks() at init time.
|
|
26
26
|
|
|
27
27
|
Silent exit 0 cases (no output):
|
|
28
28
|
- No .trellis/ directory found (not a Trellis project)
|
|
@@ -185,7 +185,7 @@ def get_active_task(root: Path, input_data: dict) -> Optional[tuple[str, str, st
|
|
|
185
185
|
if not task_dir.is_absolute():
|
|
186
186
|
task_dir = root / task_dir
|
|
187
187
|
if active.stale:
|
|
188
|
-
return task_dir.name,
|
|
188
|
+
return task_dir.name, "missing_task", active.source
|
|
189
189
|
|
|
190
190
|
task_json = task_dir / "task.json"
|
|
191
191
|
if not task_json.is_file():
|
package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
"targetPolicy": "each-existing",
|
|
9
9
|
"targets": [
|
|
10
10
|
{ "kind": "file", "path": ".codex/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
11
|
-
{ "kind": "file", "path": ".claude/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" }
|
|
11
|
+
{ "kind": "file", "path": ".claude/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
12
|
+
{ "kind": "file", "path": ".gemini/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
13
|
+
{ "kind": "file", "path": ".qoder/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
14
|
+
{ "kind": "file", "path": ".github/copilot/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
15
|
+
{ "kind": "file", "path": ".codebuddy/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
16
|
+
{ "kind": "file", "path": ".factory/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
17
|
+
{ "kind": "file", "path": ".kiro/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" },
|
|
18
|
+
{ "kind": "file", "path": ".trae/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "none" }
|
|
12
19
|
],
|
|
13
20
|
"selector": { "type": "whole-file" },
|
|
14
|
-
"baselines": ["selector.py"],
|
|
21
|
+
"baselines": ["selector.py", "baseline-flower-beta2.py", "baseline-flower-stale-task.py"],
|
|
15
22
|
"content": { "source": "content.py" }
|
|
16
23
|
}]
|
|
17
24
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "workflow-runtime-contract-reference",
|
|
4
|
+
"purpose": "workflow_state",
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "workflow-state-contract-comment",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targets": [{ "kind": "workflow", "path": ".trellis/workflow.md", "missing": "error" }],
|
|
10
|
+
"selector": { "type": "literal", "source": "state-contract-comment-selector.md" },
|
|
11
|
+
"content": { "source": "state-contract-comment-content.md" }
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "workflow-runtime-contract-reference",
|
|
15
|
+
"operation": "replace",
|
|
16
|
+
"targets": [{ "kind": "workflow", "path": ".trellis/workflow.md", "missing": "error" }],
|
|
17
|
+
"selector": { "type": "literal", "source": "runtime-reference-selector.md" },
|
|
18
|
+
"content": { "source": "runtime-reference-content.md" }
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
For the workflow state machine's runtime contract, the authoritative runtime inputs are the installed per-turn hook parser and the `[workflow-state:*]` tags in this file. This Flower variant uses fixed pseudo-status tag names `no_task` and `missing_task`; hook diagnostic source types such as `session` or `session-fallback` must not become workflow-state tag names.
|
|
2
|
+
|
|
3
|
+
- Installed `<platform>/hooks/inject-workflow-state.py` copies — parse this workflow and emit the current breadcrumb for platforms with a per-turn hook.
|
|
4
|
+
- `.trellis/spec/` project specs, when present — project-local runtime contract notes and invariants.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
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:
|
|
2
|
+
|
|
3
|
+
- `.trellis/spec/cli/backend/workflow-state-contract.md` — runtime contract + writer table + test invariants
|
|
4
|
+
- `.trellis/scripts/inject-workflow-state.py` — actual parser (reads workflow.md only, no embedded text)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below)
|
|
3
|
+
|
|
4
|
+
The [workflow-state:STATUS] blocks embedded in the ## Phase Index section
|
|
5
|
+
below are the SINGLE source of truth for the per-turn `<workflow-state>`
|
|
6
|
+
breadcrumb that every supported AI platform's UserPromptSubmit hook
|
|
7
|
+
reads. inject-workflow-state.py (Python platforms) and
|
|
8
|
+
inject-workflow-state.js (OpenCode plugin) only parse them — there is no
|
|
9
|
+
fallback dict baked into the scripts after v0.5.0-rc.0.
|
|
10
|
+
|
|
11
|
+
STATUS charset: [A-Za-z0-9_-]+. When the hook can't find a tag, it
|
|
12
|
+
degrades to a generic "Refer to workflow.md for current step." line —
|
|
13
|
+
intentionally visible so users notice and fix a broken workflow.md.
|
|
14
|
+
|
|
15
|
+
INVARIANT (test/regression.test.ts):
|
|
16
|
+
Every workflow-walkthrough step marked `[required · once]` must have a
|
|
17
|
+
matching enforcement line in its phase's [workflow-state:*] block. The
|
|
18
|
+
breadcrumb is the only per-turn channel; if a mandatory step isn't
|
|
19
|
+
mentioned there, the AI silently skips it (Phase 1 planning gate
|
|
20
|
+
skip and Phase 3.4 commit skip both manifested via this gap).
|
|
21
|
+
|
|
22
|
+
TAG ↔ PHASE scoping:
|
|
23
|
+
[workflow-state:no_task] → no active task; before Phase 1
|
|
24
|
+
[workflow-state:missing_task] → missing active-task directory recovery
|
|
25
|
+
[workflow-state:planning] → all of Phase 1 (status='planning')
|
|
26
|
+
[workflow-state:planning-inline] → Codex inline variant of Phase 1
|
|
27
|
+
[workflow-state:in_progress] → Phase 2 + Phase 3.2-3.4
|
|
28
|
+
(status stays 'in_progress' from
|
|
29
|
+
task.py start until task.py archive)
|
|
30
|
+
[workflow-state:in_progress-inline] → Codex inline variant of Phase 2/3
|
|
31
|
+
[workflow-state:completed] → currently DEAD: cmd_archive flips
|
|
32
|
+
status and moves the dir in the same
|
|
33
|
+
call, so the resolver loses the
|
|
34
|
+
pointer (block kept for a future
|
|
35
|
+
explicit in_progress→completed
|
|
36
|
+
transition)
|
|
37
|
+
|
|
38
|
+
Editing checklist:
|
|
39
|
+
- When you change a [workflow-state:STATUS] block, also check the
|
|
40
|
+
matching phase's `[required · once]` walkthrough steps for sync
|
|
41
|
+
- Run `trellis update` after editing to push the new bodies to
|
|
42
|
+
downstream user projects (block-level managed replacement)
|
|
43
|
+
- Runtime pseudo-status names are fixed. This Flower variant defines
|
|
44
|
+
`no_task` and `missing_task`; hook diagnostic source types must not be
|
|
45
|
+
appended to workflow-state tag names.
|
|
46
|
+
-->
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
WORKFLOW-STATE BREADCRUMB CONTRACT (read this before editing the tag blocks below)
|
|
3
|
+
|
|
4
|
+
The [workflow-state:STATUS] blocks embedded in the ## Phase Index section
|
|
5
|
+
below are the SINGLE source of truth for the per-turn `<workflow-state>`
|
|
6
|
+
breadcrumb that every supported AI platform's UserPromptSubmit hook
|
|
7
|
+
reads. inject-workflow-state.py (Python platforms) and
|
|
8
|
+
inject-workflow-state.js (OpenCode plugin) only parse them — there is no
|
|
9
|
+
fallback dict baked into the scripts after v0.5.0-rc.0.
|
|
10
|
+
|
|
11
|
+
STATUS charset: [A-Za-z0-9_-]+. When the hook can't find a tag, it
|
|
12
|
+
degrades to a generic "Refer to workflow.md for current step." line —
|
|
13
|
+
intentionally visible so users notice and fix a broken workflow.md.
|
|
14
|
+
|
|
15
|
+
INVARIANT (test/regression.test.ts):
|
|
16
|
+
Every workflow-walkthrough step marked `[required · once]` must have a
|
|
17
|
+
matching enforcement line in its phase's [workflow-state:*] block. The
|
|
18
|
+
breadcrumb is the only per-turn channel; if a mandatory step isn't
|
|
19
|
+
mentioned there, the AI silently skips it (Phase 1 planning gate
|
|
20
|
+
skip and Phase 3.4 commit skip both manifested via this gap).
|
|
21
|
+
|
|
22
|
+
TAG ↔ PHASE scoping:
|
|
23
|
+
[workflow-state:no_task] → no active task; before Phase 1
|
|
24
|
+
[workflow-state:planning] → all of Phase 1 (status='planning')
|
|
25
|
+
[workflow-state:planning-inline] → Codex inline variant of Phase 1
|
|
26
|
+
[workflow-state:in_progress] → Phase 2 + Phase 3.2-3.4
|
|
27
|
+
(status stays 'in_progress' from
|
|
28
|
+
task.py start until task.py archive)
|
|
29
|
+
[workflow-state:in_progress-inline] → Codex inline variant of Phase 2/3
|
|
30
|
+
[workflow-state:completed] → currently DEAD: cmd_archive flips
|
|
31
|
+
status and moves the dir in the same
|
|
32
|
+
call, so the resolver loses the
|
|
33
|
+
pointer (block kept for a future
|
|
34
|
+
explicit in_progress→completed
|
|
35
|
+
transition)
|
|
36
|
+
|
|
37
|
+
Editing checklist:
|
|
38
|
+
- When you change a [workflow-state:STATUS] block, also check the
|
|
39
|
+
matching phase's `[required · once]` walkthrough steps for sync
|
|
40
|
+
- Run `trellis update` after editing to push the new bodies to
|
|
41
|
+
downstream user projects (block-level managed replacement)
|
|
42
|
+
- Full runtime contract:
|
|
43
|
+
.trellis/spec/cli/backend/workflow-state-contract.md
|
|
44
|
+
-->
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
[workflow-state:missing_task]
|
|
2
|
+
An active task pointer that points to a missing task directory is a recovery-only state, not permission to implement, edit, create a task, start a task, or attribute work to the missing task.
|
|
3
|
+
Run `python3 ./.trellis/scripts/task.py finish`. If it fails, report the failure and stop.
|
|
4
|
+
If it succeeds, in the same turn treat the current user request as `no_task` and follow `[workflow-state:no_task]` / Request Intent Routing before any edit or task action.
|
|
5
|
+
[/workflow-state:missing_task]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "workflow-state-missing-task",
|
|
4
|
+
"purpose": "workflow_state",
|
|
5
|
+
"operations": [{
|
|
6
|
+
"id": "workflow-state-missing-task",
|
|
7
|
+
"operation": "insert",
|
|
8
|
+
"position": "after",
|
|
9
|
+
"targets": [{ "kind": "workflow", "path": ".trellis/workflow.md", "missing": "error" }],
|
|
10
|
+
"selector": { "type": "literal", "source": "selector.md" },
|
|
11
|
+
"content": { "source": "content.md" },
|
|
12
|
+
"legacyMarkers": [{ "namespace": "patch", "id": "workflow-state-stale-task" }]
|
|
13
|
+
}]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[/workflow-state:no_task]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-07-
|
|
2
|
+
"syncedAt": "2026-07-19T18:42:57.187Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "ea9117c62f620fab7a78d94b5e8e7f1582de1488",
|
|
5
5
|
"common": {
|
|
6
6
|
"codexSkills": [
|
|
7
7
|
"craft-rpa",
|
|
@@ -130,12 +130,20 @@
|
|
|
130
130
|
"commands": [],
|
|
131
131
|
"overrides": [],
|
|
132
132
|
"patchFiles": [
|
|
133
|
+
"hooks/claude-session-start/missing-task-routing/content.py",
|
|
134
|
+
"hooks/claude-session-start/missing-task-routing/patch.json",
|
|
135
|
+
"hooks/claude-session-start/missing-task-routing/selector.py",
|
|
133
136
|
"hooks/claude-session-start/no-task-routing/content.py",
|
|
134
137
|
"hooks/claude-session-start/no-task-routing/patch.json",
|
|
135
138
|
"hooks/claude-session-start/no-task-routing/selector.py",
|
|
139
|
+
"hooks/codex-session-start/missing-task-routing/content.py",
|
|
140
|
+
"hooks/codex-session-start/missing-task-routing/patch.json",
|
|
141
|
+
"hooks/codex-session-start/missing-task-routing/selector.py",
|
|
136
142
|
"hooks/codex-session-start/no-task-routing/content.py",
|
|
137
143
|
"hooks/codex-session-start/no-task-routing/patch.json",
|
|
138
144
|
"hooks/codex-session-start/no-task-routing/selector.py",
|
|
145
|
+
"hooks/inject-workflow-state/shared-runtime/baseline-flower-beta2.py",
|
|
146
|
+
"hooks/inject-workflow-state/shared-runtime/baseline-flower-stale-task.py",
|
|
139
147
|
"hooks/inject-workflow-state/shared-runtime/content.py",
|
|
140
148
|
"hooks/inject-workflow-state/shared-runtime/patch.json",
|
|
141
149
|
"hooks/inject-workflow-state/shared-runtime/selector.py",
|
|
@@ -189,6 +197,14 @@
|
|
|
189
197
|
"workflow/phase-ownership/phase-3-commit-content.md",
|
|
190
198
|
"workflow/phase-ownership/phase-3-update-spec-baseline.md",
|
|
191
199
|
"workflow/phase-ownership/phase-3-update-spec-content.md",
|
|
200
|
+
"workflow/runtime-contract-reference/patch.json",
|
|
201
|
+
"workflow/runtime-contract-reference/runtime-reference-content.md",
|
|
202
|
+
"workflow/runtime-contract-reference/runtime-reference-selector.md",
|
|
203
|
+
"workflow/runtime-contract-reference/state-contract-comment-content.md",
|
|
204
|
+
"workflow/runtime-contract-reference/state-contract-comment-selector.md",
|
|
205
|
+
"workflow/state-missing-task/content.md",
|
|
206
|
+
"workflow/state-missing-task/patch.json",
|
|
207
|
+
"workflow/state-missing-task/selector.md",
|
|
192
208
|
"workflow/state-no-task/content.md",
|
|
193
209
|
"workflow/state-no-task/patch.json",
|
|
194
210
|
"workflow/state-no-task/selector.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.3",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"commit-and-tag-version": "^12.7.3"
|
|
62
62
|
},
|
|
63
63
|
"flowerReleaseNotes": {
|
|
64
|
-
"version": "0.5.0-beta.
|
|
64
|
+
"version": "0.5.0-beta.3",
|
|
65
65
|
"source": "CHANGELOG.md",
|
|
66
|
-
"body": "###
|
|
66
|
+
"body": "### 🐛 修复 Bug Fixes\n\n* **enhancements:** 同步 missing_task 意图恢复 ([e1fae6d](https://github.com/SilentFlower/flower-trellis/commit/e1fae6d1adab0440371062b7f35316602d147c5e))\n\n\n### 🧰 维护 Maintenance\n\n* **trellis:** 同步项目 Flower 版本状态到 0.5.0-beta.2 ([855fc39](https://github.com/SilentFlower/flower-trellis/commit/855fc392eaa82cc15146a897d010bdc3a8dfbb93))",
|
|
67
67
|
"truncated": false
|
|
68
68
|
}
|
|
69
69
|
}
|