@mindfoldhq/trellis 0.6.10 → 0.7.0-beta.1
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/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +20 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +0 -8
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +222 -0
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/opencode.d.ts.map +1 -1
- package/dist/configurators/opencode.js +4 -0
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/workflow.d.ts +0 -14
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +1 -37
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.7.0-beta.0.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.1.json +9 -0
- package/dist/templates/claude/settings.json +22 -0
- package/dist/templates/codex/agents/trellis-check.toml +2 -2
- package/dist/templates/codex/agents/trellis-implement.toml +2 -2
- package/dist/templates/codex/agents/trellis-research.toml +2 -2
- package/dist/templates/codex/hooks/session-start.py +20 -1
- package/dist/templates/codex/hooks.json +25 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +2 -1
- package/dist/templates/copilot/hooks/session-start.py +20 -1
- package/dist/templates/opencode/plugins/inject-spec-context.js +121 -0
- package/dist/templates/opencode/plugins/inject-workflow-state.js +111 -10
- package/dist/templates/pi/extensions/trellis/index.ts.txt +61 -4
- package/dist/templates/shared-hooks/index.d.ts +8 -2
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +13 -1
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-spec-context.py +844 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +4 -3
- package/dist/templates/shared-hooks/inject-workflow-state.py +35 -9
- package/dist/templates/shared-hooks/session-start.py +22 -1
- package/dist/templates/trellis/config.yaml +47 -0
- package/dist/templates/trellis/index.d.ts +4 -3
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +7 -3
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +0 -45
- package/dist/templates/trellis/scripts/common/config.py +18 -0
- package/dist/templates/trellis/scripts/common/git_context.py +34 -2
- package/dist/templates/trellis/scripts/common/paths.py +28 -0
- package/dist/templates/trellis/scripts/common/spec_inject.py +439 -0
- package/dist/templates/trellis/scripts/common/spec_match.py +395 -0
- package/dist/templates/trellis/scripts/common/task_store.py +30 -0
- package/dist/templates/trellis/scripts/common/workflow_phase.py +3 -2
- package/dist/templates/trellis/scripts/common/workflow_selection.py +177 -0
- package/dist/templates/trellis/scripts/task.py +82 -0
- package/package.json +2 -2
- package/dist/templates/trellis/gitattributes.txt +0 -9
|
@@ -19,7 +19,7 @@ Common files:
|
|
|
19
19
|
| Claude Code | `.claude/settings.json` |
|
|
20
20
|
| Cursor | `.cursor/hooks.json` |
|
|
21
21
|
| Codex | `.codex/hooks.json`, `.codex/config.toml` |
|
|
22
|
-
| OpenCode | `.opencode/package.json`, `.opencode/plugins
|
|
22
|
+
| OpenCode | `.opencode/package.json`, `.opencode/plugins/*`, `.opencode/hooks/inject-spec-context.py` |
|
|
23
23
|
| Kiro | `.kiro/hooks/` + platform config |
|
|
24
24
|
| Gemini CLI | `.gemini/settings.json` |
|
|
25
25
|
| Qoder | `.qoder/settings.json` |
|
|
@@ -40,6 +40,7 @@ Whether these files exist in a project depends on which `trellis init --<platfor
|
|
|
40
40
|
| `session-start.py` | Generates session-start context. |
|
|
41
41
|
| `inject-workflow-state.py` | Parses `[workflow-state:STATUS]` blocks in `.trellis/workflow.md` and emits the body matching the current task status. Falls back to `Refer to workflow.md for current step.` when no matching block exists. |
|
|
42
42
|
| `inject-subagent-context.py` | Injects PRD, JSONL context, and related spec/research into sub-agents. |
|
|
43
|
+
| `inject-spec-context.py` | Matches path-scoped specs and manages budgeted delivery state. |
|
|
43
44
|
| `inject-shell-session-context.py` | Lets shell commands inherit Trellis session identity. |
|
|
44
45
|
|
|
45
46
|
Not every platform has every hook. Do not copy files from another platform just because a platform lacks a hook; first confirm whether that platform supports the corresponding event.
|
|
@@ -451,6 +451,25 @@ def _strip_breadcrumb_tag_blocks(content: str) -> str:
|
|
|
451
451
|
return re.sub(r"\n{3,}", "\n\n", stripped).strip()
|
|
452
452
|
|
|
453
453
|
|
|
454
|
+
def _resolve_workflow_md(root: Path, input_data: dict) -> Path:
|
|
455
|
+
"""Resolve the active task's workflow file, falling back to the global one.
|
|
456
|
+
|
|
457
|
+
The per-task resolution rule lives in common.workflow_selection inside
|
|
458
|
+
.trellis/scripts. Older installed projects may not ship that module, and
|
|
459
|
+
hooks must never crash the session — ANY failure (import error, old
|
|
460
|
+
scripts tree, resolver bug) falls back to the global workflow.md.
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
scripts_dir = root / ".trellis" / "scripts"
|
|
464
|
+
if str(scripts_dir) not in sys.path:
|
|
465
|
+
sys.path.insert(0, str(scripts_dir))
|
|
466
|
+
from common.workflow_selection import resolve_workflow_md # type: ignore[import-not-found]
|
|
467
|
+
|
|
468
|
+
return resolve_workflow_md(root, input_data, platform="copilot")
|
|
469
|
+
except Exception:
|
|
470
|
+
return root / ".trellis" / "workflow.md"
|
|
471
|
+
|
|
472
|
+
|
|
454
473
|
def _build_workflow_toc(workflow_path: Path) -> str:
|
|
455
474
|
"""Inject only the compact Phase Index summary for SessionStart."""
|
|
456
475
|
content = read_file(workflow_path)
|
|
@@ -502,7 +521,7 @@ Trellis compact SessionStart context. Use it to orient the session; load details
|
|
|
502
521
|
output.write("\n</current-state>\n\n")
|
|
503
522
|
|
|
504
523
|
output.write("<trellis-workflow>\n")
|
|
505
|
-
output.write(_build_workflow_toc(
|
|
524
|
+
output.write(_build_workflow_toc(_resolve_workflow_md(project_dir, hook_input)))
|
|
506
525
|
output.write("\n</trellis-workflow>\n\n")
|
|
507
526
|
|
|
508
527
|
output.write("<guidelines>\n")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Trellis Dynamic Spec Injection Plugin
|
|
4
|
+
*
|
|
5
|
+
* OpenCode tool hooks cannot add context to the current model turn. A FULL
|
|
6
|
+
* spec emission therefore blocks the write once with a model-visible error;
|
|
7
|
+
* the shared Python engine records the emission, so the model's retry proceeds.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { debugLog } from "../lib/trellis-context.js";
|
|
14
|
+
|
|
15
|
+
const PYTHON_CMD = process.platform === "win32" ? "python" : "python3";
|
|
16
|
+
|
|
17
|
+
function runSpecHook(directory, payload) {
|
|
18
|
+
const hookPath = join(
|
|
19
|
+
directory,
|
|
20
|
+
".opencode",
|
|
21
|
+
"hooks",
|
|
22
|
+
"inject-spec-context.py",
|
|
23
|
+
);
|
|
24
|
+
if (!existsSync(hookPath)) return null;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const stdout = execFileSync(PYTHON_CMD, [hookPath], {
|
|
28
|
+
cwd: directory,
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
input: JSON.stringify(payload),
|
|
31
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
+
timeout: 15000,
|
|
33
|
+
});
|
|
34
|
+
if (!stdout.trim()) return null;
|
|
35
|
+
const parsed = JSON.parse(stdout);
|
|
36
|
+
return parsed?.hookSpecificOutput ?? null;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
debugLog(
|
|
39
|
+
"spec",
|
|
40
|
+
"Spec hook failed open:",
|
|
41
|
+
error instanceof Error ? error.message : String(error),
|
|
42
|
+
);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildToolPayload(directory, input, output) {
|
|
48
|
+
const tool = input?.tool?.toLowerCase();
|
|
49
|
+
const args = output?.args;
|
|
50
|
+
if (!args) return null;
|
|
51
|
+
|
|
52
|
+
if (tool === "edit" || tool === "write") {
|
|
53
|
+
if (typeof args.filePath !== "string" || !args.filePath.trim()) return null;
|
|
54
|
+
return {
|
|
55
|
+
hook_event_name: "PreToolUse",
|
|
56
|
+
session_id: input?.sessionID,
|
|
57
|
+
cwd: directory,
|
|
58
|
+
tool_name: tool === "edit" ? "Edit" : "Write",
|
|
59
|
+
tool_input: { file_path: args.filePath },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (tool === "apply_patch") {
|
|
64
|
+
if (typeof args.patchText !== "string" || !args.patchText.trim())
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
hook_event_name: "PreToolUse",
|
|
68
|
+
session_id: input?.sessionID,
|
|
69
|
+
cwd: directory,
|
|
70
|
+
tool_name: "apply_patch",
|
|
71
|
+
tool_input: { command: args.patchText },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default async ({ directory }) => ({
|
|
79
|
+
event: async ({ event }) => {
|
|
80
|
+
if (
|
|
81
|
+
process.env.TRELLIS_HOOKS === "0" ||
|
|
82
|
+
process.env.TRELLIS_DISABLE_HOOKS === "1"
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event?.type !== "session.compacted" || !event?.properties?.sessionID) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
runSpecHook(directory, {
|
|
91
|
+
hook_event_name: "SessionStart",
|
|
92
|
+
session_id: event.properties.sessionID,
|
|
93
|
+
source: "compact",
|
|
94
|
+
cwd: directory,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"tool.execute.before": async (input, output) => {
|
|
99
|
+
if (
|
|
100
|
+
process.env.TRELLIS_HOOKS === "0" ||
|
|
101
|
+
process.env.TRELLIS_DISABLE_HOOKS === "1"
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const payload = buildToolPayload(directory, input, output);
|
|
107
|
+
if (!payload) return;
|
|
108
|
+
|
|
109
|
+
const result = runSpecHook(directory, payload);
|
|
110
|
+
const context = result?.additionalContext;
|
|
111
|
+
if (result?.permissionDecision !== "deny" || typeof context !== "string") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Trellis loaded governing specs before this tool call. " +
|
|
117
|
+
"Follow them, then retry the same tool call.\n\n" +
|
|
118
|
+
context,
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* global process */
|
|
1
|
+
/* global process, console */
|
|
2
2
|
/**
|
|
3
3
|
* Trellis Workflow State Injection Plugin
|
|
4
4
|
*
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* <workflow-state> breadcrumb reminding the main AI what task is
|
|
9
9
|
* active and its expected flow. Breadcrumb text is pulled exclusively
|
|
10
10
|
* from the project's workflow.md [workflow-state:STATUS] tag blocks —
|
|
11
|
-
* workflow.md is the single source of truth.
|
|
11
|
+
* workflow.md is the single source of truth. When the active task's
|
|
12
|
+
* task.json selects a workflow variant ("workflow": "<id>"), the tag
|
|
13
|
+
* blocks are read from .trellis/workflows/<id>.md instead (missing
|
|
14
|
+
* variant file → one stderr warning + fallback through personal/team/global
|
|
15
|
+
* defaults; see
|
|
16
|
+
* resolveWorkflowMd). There are no fallback
|
|
12
17
|
* tables in this plugin: when workflow.md is missing or a tag is
|
|
13
18
|
* absent, the breadcrumb degrades to a generic
|
|
14
19
|
* "Refer to workflow.md for current step." line so users see (and fix)
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
* - task.json malformed or missing status
|
|
24
29
|
*/
|
|
25
30
|
|
|
26
|
-
import { existsSync, readFileSync } from "fs"
|
|
31
|
+
import { existsSync, readFileSync, statSync } from "fs"
|
|
27
32
|
import { join } from "path"
|
|
28
33
|
import { TrellisContext, debugLog, isTrellisSubagent } from "../lib/trellis-context.js"
|
|
29
34
|
|
|
@@ -107,17 +112,113 @@ function promptHasSkipKeyword(text, keyword) {
|
|
|
107
112
|
return pattern.test(text)
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
// Per-task workflow selection (mirrors the Python resolver in
|
|
116
|
+
// .trellis/scripts/common/workflow_selection.py). Ids are restricted to
|
|
117
|
+
// [A-Za-z0-9_-]+ so a task.json value can never traverse outside
|
|
118
|
+
// .trellis/workflows/.
|
|
119
|
+
const WORKFLOW_ID_RE = /^[A-Za-z0-9_-]+$/
|
|
120
|
+
|
|
121
|
+
// Map a workflow id to its .trellis/workflows/<id>.md file if valid and
|
|
122
|
+
// present, else "". Shared by every resolution layer. Never throws.
|
|
123
|
+
function libraryVariant(directory, workflowId) {
|
|
124
|
+
if (!workflowId || !WORKFLOW_ID_RE.test(workflowId)) return ""
|
|
125
|
+
const variantPath = join(directory, ".trellis", "workflows", `${workflowId}.md`)
|
|
126
|
+
try {
|
|
127
|
+
if (statSync(variantPath).isFile()) return variantPath
|
|
128
|
+
} catch {
|
|
129
|
+
// ENOENT etc. — missing variant file.
|
|
130
|
+
}
|
|
131
|
+
return ""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Personal override id from the gitignored .developer file (`workflow=<id>`
|
|
135
|
+
// line). "" when absent/unreadable. Never throws.
|
|
136
|
+
function developerWorkflowId(directory) {
|
|
137
|
+
try {
|
|
138
|
+
const text = readFileSync(join(directory, ".trellis", ".developer"), "utf-8")
|
|
139
|
+
for (const line of text.split(/\r?\n/)) {
|
|
140
|
+
if (line.startsWith("workflow=")) return line.slice("workflow=".length).trim()
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// No .developer / unreadable — no personal override.
|
|
144
|
+
}
|
|
145
|
+
return ""
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Team-shared default id from config.yaml's top-level `default_workflow:` key.
|
|
149
|
+
// Commented lines (starting with '#') are naturally excluded by the anchor.
|
|
150
|
+
// "" when unset/unreadable. Never throws.
|
|
151
|
+
const DEFAULT_WORKFLOW_RE = /^default_workflow:\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/m
|
|
152
|
+
function configDefaultWorkflowId(directory) {
|
|
153
|
+
try {
|
|
154
|
+
const text = readFileSync(join(directory, ".trellis", "config.yaml"), "utf-8")
|
|
155
|
+
const m = text.match(DEFAULT_WORKFLOW_RE)
|
|
156
|
+
if (m) return m[2]
|
|
157
|
+
} catch {
|
|
158
|
+
// No config.yaml / unreadable — no team default.
|
|
159
|
+
}
|
|
160
|
+
return ""
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve which workflow markdown file feeds this turn's breadcrumbs.
|
|
165
|
+
*
|
|
166
|
+
* Precedence (identical across all Trellis consumers), highest to lowest —
|
|
167
|
+
* each layer resolves an id to .trellis/workflows/<id>.md and falls through
|
|
168
|
+
* when unset, invalid, or naming a missing file:
|
|
169
|
+
* 1. Per-task pin — active task's task.json "workflow" (session-bound; a
|
|
170
|
+
* bad id / missing file warns once on stderr, then falls through).
|
|
171
|
+
* 2. Personal — .developer `workflow=<id>` (gitignored, per-developer).
|
|
172
|
+
* 3. Team default — config.yaml `default_workflow` (git-tracked, shared).
|
|
173
|
+
* 4. Global — .trellis/workflow.md.
|
|
174
|
+
* With neither a pin nor the personal/team keys set, this is the global path.
|
|
175
|
+
* Never throws.
|
|
176
|
+
*/
|
|
177
|
+
function resolveWorkflowMd(ctx, directory, platformInput) {
|
|
178
|
+
const globalPath = join(directory, ".trellis", "workflow.md")
|
|
179
|
+
|
|
180
|
+
// 1. Per-task pin (session-bound explicit choice).
|
|
181
|
+
let workflowId = ""
|
|
182
|
+
try {
|
|
183
|
+
const active = ctx.getActiveTask(platformInput)
|
|
184
|
+
if (active.taskPath && !active.stale) {
|
|
185
|
+
const taskDir = ctx.resolveTaskDir(active.taskPath)
|
|
186
|
+
if (taskDir) {
|
|
187
|
+
const data = JSON.parse(readFileSync(join(taskDir, "task.json"), "utf-8"))
|
|
188
|
+
if (data && typeof data.workflow === "string") workflowId = data.workflow
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// No active task / unreadable task.json — not a selection.
|
|
193
|
+
workflowId = ""
|
|
194
|
+
}
|
|
195
|
+
if (workflowId) {
|
|
196
|
+
const pinned = libraryVariant(directory, workflowId)
|
|
197
|
+
if (pinned) return pinned
|
|
198
|
+
console.error(
|
|
199
|
+
`Warning: active task selects workflow ${JSON.stringify(workflowId)} but .trellis/workflows/ has no matching file; using default workflow resolution`,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 2. Personal override, then 3. team default — silent on miss (defaults,
|
|
204
|
+
// not an explicit per-task choice). 4. Global fallback.
|
|
205
|
+
const personal = libraryVariant(directory, developerWorkflowId(directory))
|
|
206
|
+
if (personal) return personal
|
|
207
|
+
const team = libraryVariant(directory, configDefaultWorkflowId(directory))
|
|
208
|
+
if (team) return team
|
|
209
|
+
return globalPath
|
|
210
|
+
}
|
|
211
|
+
|
|
110
212
|
/**
|
|
111
|
-
* Parse workflow
|
|
213
|
+
* Parse the resolved workflow markdown for [workflow-state:STATUS] blocks.
|
|
112
214
|
*
|
|
113
|
-
* Returns {status: body}. workflow
|
|
114
|
-
* there are no fallback tables here. Missing tags (or a missing /
|
|
115
|
-
* unreadable workflow
|
|
215
|
+
* Returns {status: body}. The workflow file is the single source of
|
|
216
|
+
* truth — there are no fallback tables here. Missing tags (or a missing /
|
|
217
|
+
* unreadable workflow file) fall back to a generic line in
|
|
116
218
|
* buildBreadcrumb so users see the broken state and fix workflow.md
|
|
117
219
|
* rather than the plugin silently masking it.
|
|
118
220
|
*/
|
|
119
|
-
function loadBreadcrumbs(
|
|
120
|
-
const workflowPath = join(directory, ".trellis", "workflow.md")
|
|
221
|
+
function loadBreadcrumbs(workflowPath) {
|
|
121
222
|
if (!existsSync(workflowPath)) return {}
|
|
122
223
|
let content
|
|
123
224
|
try {
|
|
@@ -214,7 +315,7 @@ export default async ({ directory }) => {
|
|
|
214
315
|
return
|
|
215
316
|
}
|
|
216
317
|
|
|
217
|
-
const templates = loadBreadcrumbs(directory)
|
|
318
|
+
const templates = loadBreadcrumbs(resolveWorkflowMd(ctx, directory, input))
|
|
218
319
|
const task = getActiveTask(ctx, input)
|
|
219
320
|
const breadcrumb = task
|
|
220
321
|
? buildBreadcrumb(task.id, task.status, templates, task.source)
|
|
@@ -758,10 +758,11 @@ function truncateUtf8(buf: Buffer, cap: number): Buffer {
|
|
|
758
758
|
if ((lead & 0xe0) === 0xc0) seqLen = 2;
|
|
759
759
|
else if ((lead & 0xf0) === 0xe0) seqLen = 3;
|
|
760
760
|
else if ((lead & 0xf8) === 0xf0) seqLen = 4;
|
|
761
|
-
//
|
|
762
|
-
|
|
761
|
+
// Cut before the lead byte when its full sequence didn't fit;
|
|
762
|
+
// otherwise the trailing sequence is complete — keep it whole.
|
|
763
|
+
if (i - 1 + seqLen > cap) return buf.subarray(0, i - 1);
|
|
763
764
|
}
|
|
764
|
-
return buf.subarray(0,
|
|
765
|
+
return buf.subarray(0, cap);
|
|
765
766
|
}
|
|
766
767
|
|
|
767
768
|
function stripInlineComment(value: string): string {
|
|
@@ -1080,10 +1081,66 @@ function adoptKey(root: string, key: string): string {
|
|
|
1080
1081
|
}
|
|
1081
1082
|
|
|
1082
1083
|
// ── Workflow State Breadcrumb ─────────────────────────────────────────
|
|
1084
|
+
const WORKFLOW_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
1085
|
+
const DEFAULT_WORKFLOW_RE =
|
|
1086
|
+
/^default_workflow:\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/m;
|
|
1087
|
+
|
|
1088
|
+
function workflowVariant(root: string, workflowId: string): string {
|
|
1089
|
+
if (!WORKFLOW_ID_RE.test(workflowId)) return "";
|
|
1090
|
+
const path = join(root, ".trellis", "workflows", `${workflowId}.md`);
|
|
1091
|
+
return exists(path) ? path : "";
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function developerWorkflowId(root: string): string {
|
|
1095
|
+
for (const line of readText(join(root, ".trellis", ".developer")).split(
|
|
1096
|
+
/\r?\n/,
|
|
1097
|
+
)) {
|
|
1098
|
+
if (line.startsWith("workflow="))
|
|
1099
|
+
return line.slice("workflow=".length).trim();
|
|
1100
|
+
}
|
|
1101
|
+
return "";
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function configDefaultWorkflowId(root: string): string {
|
|
1105
|
+
return (
|
|
1106
|
+
readText(join(root, ".trellis", "config.yaml")).match(
|
|
1107
|
+
DEFAULT_WORKFLOW_RE,
|
|
1108
|
+
)?.[2] ?? ""
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Mirrors common/workflow_selection.py without spawning another Python
|
|
1113
|
+
* process on every Pi turn. Invalid or missing selections fall through. */
|
|
1114
|
+
function resolveWorkflowMd(root: string, key: string | null): string {
|
|
1115
|
+
const taskDir = readTaskDir(root, key);
|
|
1116
|
+
let workflowId = "";
|
|
1117
|
+
if (taskDir) {
|
|
1118
|
+
try {
|
|
1119
|
+
const task = JSON.parse(
|
|
1120
|
+
readText(join(taskDir, "task.json")),
|
|
1121
|
+
) as JsonObject;
|
|
1122
|
+
workflowId = typeof task.workflow === "string" ? task.workflow : "";
|
|
1123
|
+
} catch {}
|
|
1124
|
+
}
|
|
1125
|
+
if (workflowId) {
|
|
1126
|
+
const pinned = workflowVariant(root, workflowId);
|
|
1127
|
+
if (pinned) return pinned;
|
|
1128
|
+
console.error(
|
|
1129
|
+
`Warning: active task selects workflow ${JSON.stringify(workflowId)} but .trellis/workflows/ has no matching file; using default workflow resolution`,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const personal = workflowVariant(root, developerWorkflowId(root));
|
|
1134
|
+
if (personal) return personal;
|
|
1135
|
+
const team = workflowVariant(root, configDefaultWorkflowId(root));
|
|
1136
|
+
if (team) return team;
|
|
1137
|
+
return join(root, ".trellis", "workflow.md");
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1083
1140
|
const WF_RE =
|
|
1084
1141
|
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
1085
1142
|
function workflowBreadcrumb(root: string, key: string | null): string {
|
|
1086
|
-
const wf = readText(
|
|
1143
|
+
const wf = readText(resolveWorkflowMd(root, key));
|
|
1087
1144
|
if (!wf) return "";
|
|
1088
1145
|
const templates: Record<string, string> = {};
|
|
1089
1146
|
for (const m of wf.matchAll(WF_RE)) {
|
|
@@ -11,8 +11,8 @@ export interface HookScript {
|
|
|
11
11
|
/** Script content — no placeholders, ready to write directly */
|
|
12
12
|
content: string;
|
|
13
13
|
}
|
|
14
|
-
export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py";
|
|
15
|
-
export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro" | "trae" | "zcode";
|
|
14
|
+
export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py" | "inject-spec-context.py";
|
|
15
|
+
export type SharedHookPlatform = "claude" | "cursor" | "codex" | "opencode" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro" | "trae" | "zcode";
|
|
16
16
|
/**
|
|
17
17
|
* Which shared hooks each platform actually invokes. Single source of truth
|
|
18
18
|
* for shared-hook distribution — both `writeSharedHooks` (runtime install)
|
|
@@ -38,6 +38,12 @@ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qod
|
|
|
38
38
|
* (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
|
|
39
39
|
* spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
|
|
40
40
|
* stdout directly to the conversation context (no JSON envelope).
|
|
41
|
+
* - `inject-spec-context.py` — path-scoped spec injection. Claude Code uses
|
|
42
|
+
* PostToolUse Read/Edit/Write/MultiEdit; Codex and OpenCode use PreToolUse.
|
|
43
|
+
* Codex passes the native apply_patch payload directly. OpenCode's plugin
|
|
44
|
+
* adapts write/edit/apply_patch calls and blocks a FULL emission once so the
|
|
45
|
+
* model sees the specs before retrying. Class-2 platforms use
|
|
46
|
+
* `get_context.py --mode spec` pull mode instead.
|
|
41
47
|
* - Claude Code `statusLine` is intentionally not installed by default.
|
|
42
48
|
* Users can add their own statusLine command in `.claude/settings.json`,
|
|
43
49
|
* or opt in to the Trellis one via `trellis init --with-statusline`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,GAC5B,wBAAwB,CAAC;AAE7B,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CA2C1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
|
|
@@ -38,6 +38,12 @@ function readTemplate(relativePath) {
|
|
|
38
38
|
* (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
|
|
39
39
|
* spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
|
|
40
40
|
* stdout directly to the conversation context (no JSON envelope).
|
|
41
|
+
* - `inject-spec-context.py` — path-scoped spec injection. Claude Code uses
|
|
42
|
+
* PostToolUse Read/Edit/Write/MultiEdit; Codex and OpenCode use PreToolUse.
|
|
43
|
+
* Codex passes the native apply_patch payload directly. OpenCode's plugin
|
|
44
|
+
* adapts write/edit/apply_patch calls and blocks a FULL emission once so the
|
|
45
|
+
* model sees the specs before retrying. Class-2 platforms use
|
|
46
|
+
* `get_context.py --mode spec` pull mode instead.
|
|
41
47
|
* - Claude Code `statusLine` is intentionally not installed by default.
|
|
42
48
|
* Users can add their own statusLine command in `.claude/settings.json`,
|
|
43
49
|
* or opt in to the Trellis one via `trellis init --with-statusline`
|
|
@@ -54,13 +60,19 @@ export const SHARED_HOOKS_BY_PLATFORM = {
|
|
|
54
60
|
"session-start.py",
|
|
55
61
|
"inject-workflow-state.py",
|
|
56
62
|
"inject-subagent-context.py",
|
|
63
|
+
"inject-spec-context.py",
|
|
57
64
|
],
|
|
58
65
|
cursor: [
|
|
59
66
|
"session-start.py",
|
|
60
67
|
"inject-shell-session-context.py",
|
|
61
68
|
"inject-subagent-context.py",
|
|
62
69
|
],
|
|
63
|
-
codex: [
|
|
70
|
+
codex: [
|
|
71
|
+
"inject-workflow-state.py",
|
|
72
|
+
"inject-subagent-context.py",
|
|
73
|
+
"inject-spec-context.py",
|
|
74
|
+
],
|
|
75
|
+
opencode: ["inject-spec-context.py"],
|
|
64
76
|
gemini: ["session-start.py", "inject-workflow-state.py"],
|
|
65
77
|
qoder: ["session-start.py", "inject-workflow-state.py"],
|
|
66
78
|
copilot: ["inject-workflow-state.py"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,wBAAwB;KACzB;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,0BAA0B;QAC1B,4BAA4B;QAC5B,wBAAwB;KACzB;IACD,QAAQ,EAAE,CAAC,wBAAwB,CAAC;IACpC,MAAM,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,KAAK,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACvD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACtD,KAAK,EAAE;QACL,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
|