@mindfoldhq/trellis 0.5.0-beta.13 → 0.5.0-beta.15
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/README.md +5 -5
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +24 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +15 -12
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/claude.js +1 -1
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codebuddy.js +1 -1
- package/dist/configurators/codebuddy.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +3 -6
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +4 -11
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/cursor.js +1 -1
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/droid.js +1 -1
- package/dist/configurators/droid.js.map +1 -1
- package/dist/configurators/gemini.d.ts.map +1 -1
- package/dist/configurators/gemini.js +1 -3
- package/dist/configurators/gemini.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +24 -38
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kiro.js +1 -1
- package/dist/configurators/kiro.js.map +1 -1
- package/dist/configurators/pi.d.ts +3 -0
- package/dist/configurators/pi.d.ts.map +1 -0
- package/dist/configurators/pi.js +39 -0
- package/dist/configurators/pi.js.map +1 -0
- package/dist/configurators/qoder.d.ts.map +1 -1
- package/dist/configurators/qoder.js +1 -3
- package/dist/configurators/qoder.js.map +1 -1
- package/dist/configurators/shared.d.ts +2 -4
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +6 -9
- package/dist/configurators/shared.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.15.json +126 -0
- package/dist/templates/claude/agents/trellis-research.md +1 -1
- package/dist/templates/claude/settings.json +0 -4
- package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
- package/dist/templates/codex/agents/trellis-check.toml +0 -16
- package/dist/templates/codex/agents/trellis-implement.toml +0 -16
- package/dist/templates/codex/agents/trellis-research.toml +3 -2
- package/dist/templates/codex/hooks/session-start.py +82 -22
- package/dist/templates/codex/skills/start/SKILL.md +1 -1
- package/dist/templates/copilot/hooks/session-start.py +84 -26
- package/dist/templates/copilot/prompts/start.prompt.md +1 -1
- package/dist/templates/cursor/agents/trellis-check.md +1 -1
- package/dist/templates/cursor/agents/trellis-implement.md +1 -1
- package/dist/templates/cursor/agents/trellis-research.md +2 -2
- package/dist/templates/cursor/hooks.json +7 -1
- package/dist/templates/droid/droids/trellis-research.md +1 -1
- package/dist/templates/extract.d.ts +6 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +14 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/gemini/agents/trellis-research.md +1 -1
- package/dist/templates/kiro/agents/trellis-research.json +1 -1
- package/dist/templates/markdown/agents.md +11 -12
- package/dist/templates/markdown/gitignore.txt +3 -0
- package/dist/templates/opencode/agents/trellis-check.md +1 -1
- package/dist/templates/opencode/agents/trellis-implement.md +1 -1
- package/dist/templates/opencode/agents/trellis-research.md +2 -2
- package/dist/templates/opencode/lib/trellis-context.js +100 -13
- package/dist/templates/opencode/plugins/inject-subagent-context.js +54 -4
- package/dist/templates/opencode/plugins/inject-workflow-state.js +50 -23
- package/dist/templates/opencode/plugins/session-start.js +46 -21
- package/dist/templates/pi/agents/trellis-check.md +28 -0
- package/dist/templates/pi/agents/trellis-implement.md +33 -0
- package/dist/templates/pi/agents/trellis-research.md +25 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +549 -0
- package/dist/templates/pi/index.d.ts +5 -0
- package/dist/templates/pi/index.d.ts.map +1 -0
- package/dist/templates/pi/index.js +12 -0
- package/dist/templates/pi/index.js.map +1 -0
- package/dist/templates/pi/settings.json +12 -0
- package/dist/templates/qoder/agents/trellis-research.md +1 -1
- package/dist/templates/shared-hooks/index.d.ts +31 -0
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +59 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +128 -26
- package/dist/templates/shared-hooks/inject-workflow-state.py +101 -61
- package/dist/templates/shared-hooks/session-start.py +151 -28
- package/dist/templates/trellis/gitignore.txt +3 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/__init__.py +8 -0
- package/dist/templates/trellis/scripts/common/active_task.py +593 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +43 -8
- package/dist/templates/trellis/scripts/common/paths.py +61 -58
- package/dist/templates/trellis/scripts/common/session_context.py +12 -0
- package/dist/templates/trellis/scripts/common/task_store.py +4 -6
- package/dist/templates/trellis/scripts/task.py +56 -14
- package/dist/templates/trellis/workflow.md +31 -26
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +16 -0
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +22 -6
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +405 -27
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +3 -2
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/shared-hooks/statusline.py +0 -218
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type JsonObject = Record<string, unknown>;
|
|
7
|
+
type TextContent = { type: "text"; text: string };
|
|
8
|
+
|
|
9
|
+
interface PiToolResult {
|
|
10
|
+
content: TextContent[];
|
|
11
|
+
details?: JsonObject;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PiExtensionContext {
|
|
15
|
+
hasUI?: boolean;
|
|
16
|
+
sessionManager?: {
|
|
17
|
+
getSessionId?: () => string;
|
|
18
|
+
getSessionFile?: () => string | undefined;
|
|
19
|
+
};
|
|
20
|
+
ui?: {
|
|
21
|
+
notify?: (message: string, type?: "info" | "warning" | "error") => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PiBeforeAgentStartEvent {
|
|
26
|
+
systemPrompt?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PiContextEvent {
|
|
30
|
+
messages?: unknown[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PiToolCallEvent {
|
|
34
|
+
toolName?: string;
|
|
35
|
+
input?: JsonObject;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SubagentInput {
|
|
39
|
+
agent?: string;
|
|
40
|
+
prompt?: string;
|
|
41
|
+
mode?: "single" | "parallel" | "chain";
|
|
42
|
+
prompts?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TRELLIS_AGENT_JSONL: Record<string, string> = {
|
|
46
|
+
"trellis-implement": "implement.jsonl",
|
|
47
|
+
implement: "implement.jsonl",
|
|
48
|
+
"trellis-check": "check.jsonl",
|
|
49
|
+
check: "check.jsonl",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function findProjectRoot(startDir: string): string {
|
|
53
|
+
let current = resolve(startDir);
|
|
54
|
+
while (true) {
|
|
55
|
+
if (
|
|
56
|
+
existsSync(join(current, ".trellis")) ||
|
|
57
|
+
existsSync(join(current, ".pi"))
|
|
58
|
+
) {
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
const parent = dirname(current);
|
|
62
|
+
if (parent === current) return resolve(startDir);
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readText(path: string): string {
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(path, "utf-8");
|
|
70
|
+
} catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stripMarkdownFrontmatter(content: string): string {
|
|
76
|
+
const normalized = content.replace(/^\uFEFF/, "");
|
|
77
|
+
const match = normalized.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
78
|
+
return (match ? normalized.slice(match[0].length) : normalized).trimStart();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toPiPromptArgument(prompt: string): string {
|
|
82
|
+
return prompt.startsWith("-") ? `\n${prompt}` : prompt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stringValue(value: unknown): string | null {
|
|
90
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizeKey(raw: string): string {
|
|
94
|
+
return raw
|
|
95
|
+
.trim()
|
|
96
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
97
|
+
.replace(/^[._-]+|[._-]+$/g, "")
|
|
98
|
+
.slice(0, 160);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hashValue(raw: string): string {
|
|
102
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createProcessContextKey(projectRoot: string): string {
|
|
106
|
+
return `pi_process_${hashValue(
|
|
107
|
+
[projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"),
|
|
108
|
+
)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function callString(callback: (() => string | undefined) | undefined): string | null {
|
|
112
|
+
if (!callback) return null;
|
|
113
|
+
try {
|
|
114
|
+
return stringValue(callback());
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function lookupString(data: unknown, keys: string[]): string | null {
|
|
121
|
+
if (!isJsonObject(data)) return null;
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
const value = stringValue(data[key]);
|
|
124
|
+
if (value) return value;
|
|
125
|
+
}
|
|
126
|
+
for (const nestedKey of ["input", "properties", "event", "hook_input", "hookInput"]) {
|
|
127
|
+
const nested = data[nestedKey];
|
|
128
|
+
const value = lookupString(nested, keys);
|
|
129
|
+
if (value) return value;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractTextContent(content: unknown): string {
|
|
135
|
+
if (typeof content === "string") return content;
|
|
136
|
+
if (!Array.isArray(content)) return "";
|
|
137
|
+
|
|
138
|
+
return content
|
|
139
|
+
.map((block) => {
|
|
140
|
+
if (!isJsonObject(block)) return "";
|
|
141
|
+
return block.type === "text" && typeof block.text === "string"
|
|
142
|
+
? block.text
|
|
143
|
+
: "";
|
|
144
|
+
})
|
|
145
|
+
.join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractFinalAssistantText(output: string): string | null {
|
|
149
|
+
let finalText = "";
|
|
150
|
+
|
|
151
|
+
for (const line of output.split(/\r?\n/)) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (!trimmed) continue;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const event = JSON.parse(trimmed) as JsonObject;
|
|
157
|
+
const message = isJsonObject(event.message) ? event.message : null;
|
|
158
|
+
if (message?.role !== "assistant") continue;
|
|
159
|
+
|
|
160
|
+
const text = extractTextContent(message.content);
|
|
161
|
+
if (text) finalText = text;
|
|
162
|
+
} catch {
|
|
163
|
+
// Pi can print non-JSON diagnostics around JSON mode; keep scanning.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return finalText || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatPiOutput(stdout: string, stderr: string): string {
|
|
171
|
+
return extractFinalAssistantText(stdout) ?? (stdout || stderr);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeTaskRef(raw: string): string | null {
|
|
175
|
+
let normalized = raw.trim().replace(/\\/g, "/");
|
|
176
|
+
if (!normalized) return null;
|
|
177
|
+
while (normalized.startsWith("./")) normalized = normalized.slice(2);
|
|
178
|
+
if (normalized.startsWith("tasks/")) normalized = `.trellis/${normalized}`;
|
|
179
|
+
return normalized;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function taskRefToDir(projectRoot: string, taskRef: string): string {
|
|
183
|
+
if (taskRef.startsWith("/")) return taskRef;
|
|
184
|
+
if (taskRef.startsWith(".trellis/")) return join(projectRoot, taskRef);
|
|
185
|
+
return join(projectRoot, ".trellis", "tasks", taskRef);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveContextKey(
|
|
189
|
+
input: unknown,
|
|
190
|
+
ctx?: PiExtensionContext,
|
|
191
|
+
fallback?: string | null,
|
|
192
|
+
): string | null {
|
|
193
|
+
const override = stringValue(process.env.TRELLIS_CONTEXT_ID);
|
|
194
|
+
if (override) return sanitizeKey(override) || hashValue(override);
|
|
195
|
+
|
|
196
|
+
const sessionId =
|
|
197
|
+
callString(ctx?.sessionManager?.getSessionId) ??
|
|
198
|
+
stringValue(process.env.PI_SESSION_ID) ??
|
|
199
|
+
stringValue(process.env.PI_SESSIONID) ??
|
|
200
|
+
lookupString(input, ["session_id", "sessionId", "sessionID"]);
|
|
201
|
+
if (sessionId) return `pi_${sanitizeKey(sessionId) || hashValue(sessionId)}`;
|
|
202
|
+
|
|
203
|
+
const transcriptPath =
|
|
204
|
+
callString(ctx?.sessionManager?.getSessionFile) ??
|
|
205
|
+
lookupString(input, [
|
|
206
|
+
"transcript_path",
|
|
207
|
+
"transcriptPath",
|
|
208
|
+
"transcript",
|
|
209
|
+
]);
|
|
210
|
+
if (transcriptPath) return `pi_transcript_${hashValue(transcriptPath)}`;
|
|
211
|
+
|
|
212
|
+
return fallback ?? null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readCurrentTask(
|
|
216
|
+
projectRoot: string,
|
|
217
|
+
platformInput?: unknown,
|
|
218
|
+
ctx?: PiExtensionContext,
|
|
219
|
+
contextKeyOverride?: string | null,
|
|
220
|
+
): string | null {
|
|
221
|
+
const contextKey = resolveContextKey(platformInput, ctx, contextKeyOverride);
|
|
222
|
+
if (contextKey) {
|
|
223
|
+
try {
|
|
224
|
+
const rawContext = readText(
|
|
225
|
+
join(projectRoot, ".trellis", ".runtime", "sessions", `${contextKey}.json`),
|
|
226
|
+
);
|
|
227
|
+
const context = JSON.parse(rawContext) as JsonObject;
|
|
228
|
+
const taskRef = normalizeTaskRef(stringValue(context.current_task) ?? "");
|
|
229
|
+
if (taskRef) return taskRefToDir(projectRoot, taskRef);
|
|
230
|
+
} catch {
|
|
231
|
+
// Missing or malformed session context means no active task.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readJsonlFiles(
|
|
239
|
+
projectRoot: string,
|
|
240
|
+
taskDir: string,
|
|
241
|
+
jsonlName: string,
|
|
242
|
+
): string {
|
|
243
|
+
const jsonlPath = join(taskDir, jsonlName);
|
|
244
|
+
const lines = readText(jsonlPath).split(/\r?\n/);
|
|
245
|
+
const chunks: string[] = [];
|
|
246
|
+
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
const trimmed = line.trim();
|
|
249
|
+
if (!trimmed) continue;
|
|
250
|
+
try {
|
|
251
|
+
const row = JSON.parse(trimmed) as JsonObject;
|
|
252
|
+
const file = typeof row.file === "string" ? row.file : "";
|
|
253
|
+
if (!file) continue;
|
|
254
|
+
const content = readText(join(projectRoot, file));
|
|
255
|
+
if (content) {
|
|
256
|
+
chunks.push(`## ${file}\n\n${content}`);
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Seed rows and malformed lines must not block sub-agent startup.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return chunks.join("\n\n---\n\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildTrellisContext(
|
|
267
|
+
projectRoot: string,
|
|
268
|
+
agent: string,
|
|
269
|
+
platformInput?: unknown,
|
|
270
|
+
ctx?: PiExtensionContext,
|
|
271
|
+
contextKey?: string | null,
|
|
272
|
+
): string {
|
|
273
|
+
const taskDir = readCurrentTask(projectRoot, platformInput, ctx, contextKey);
|
|
274
|
+
if (!taskDir) {
|
|
275
|
+
return "No active Trellis task found. Read .trellis/ before proceeding.";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const prd = readText(join(taskDir, "prd.md"));
|
|
279
|
+
const info = readText(join(taskDir, "info.md"));
|
|
280
|
+
const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
|
|
281
|
+
const specContext = jsonlName
|
|
282
|
+
? readJsonlFiles(projectRoot, taskDir, jsonlName)
|
|
283
|
+
: "";
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
"## Trellis Task Context",
|
|
287
|
+
`Task directory: ${taskDir}`,
|
|
288
|
+
"",
|
|
289
|
+
"### prd.md",
|
|
290
|
+
prd || "(missing)",
|
|
291
|
+
info ? "\n### info.md\n" + info : "",
|
|
292
|
+
specContext ? "\n### Curated Spec / Research Context\n" + specContext : "",
|
|
293
|
+
].join("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readAgentDefinition(projectRoot: string, agent: string): string {
|
|
297
|
+
const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
|
|
298
|
+
return stripMarkdownFrontmatter(
|
|
299
|
+
readText(join(projectRoot, ".pi", "agents", `${normalized}.md`)),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandStartsWithTrellisContext(command: string): boolean {
|
|
304
|
+
const trimmed = command.trimStart();
|
|
305
|
+
return (
|
|
306
|
+
/^export\s+TRELLIS_CONTEXT_ID=/.test(trimmed) ||
|
|
307
|
+
/^TRELLIS_CONTEXT_ID=/.test(trimmed) ||
|
|
308
|
+
/^env\s+.*\bTRELLIS_CONTEXT_ID=/.test(trimmed)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function shellQuote(value: string): string {
|
|
313
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function injectTrellisContextIntoBash(
|
|
317
|
+
event: unknown,
|
|
318
|
+
contextKey: string,
|
|
319
|
+
): boolean {
|
|
320
|
+
const toolCall = event as PiToolCallEvent;
|
|
321
|
+
if (toolCall.toolName !== "bash" || !isJsonObject(toolCall.input)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const rawCommand = toolCall.input.command;
|
|
326
|
+
if (typeof rawCommand !== "string" || !rawCommand.trim()) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (commandStartsWithTrellisContext(rawCommand)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
toolCall.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ${rawCommand}`;
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function runPi(
|
|
338
|
+
projectRoot: string,
|
|
339
|
+
prompt: string,
|
|
340
|
+
contextKey?: string | null,
|
|
341
|
+
): Promise<string> {
|
|
342
|
+
return new Promise((resolvePromise, reject) => {
|
|
343
|
+
const child = spawn(
|
|
344
|
+
"pi",
|
|
345
|
+
["--mode", "json", "-p", "--no-session", toPiPromptArgument(prompt)],
|
|
346
|
+
{
|
|
347
|
+
cwd: projectRoot,
|
|
348
|
+
env: contextKey
|
|
349
|
+
? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
|
|
350
|
+
: process.env,
|
|
351
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const stdout: Buffer[] = [];
|
|
356
|
+
const stderr: Buffer[] = [];
|
|
357
|
+
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
|
|
358
|
+
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
|
|
359
|
+
child.on("error", reject);
|
|
360
|
+
child.on("close", (code) => {
|
|
361
|
+
const out = Buffer.concat(stdout).toString("utf-8");
|
|
362
|
+
const err = Buffer.concat(stderr).toString("utf-8");
|
|
363
|
+
if (code === 0) {
|
|
364
|
+
resolvePromise(formatPiOutput(out, err));
|
|
365
|
+
} else {
|
|
366
|
+
reject(new Error(err || `pi exited with code ${code ?? "unknown"}`));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildSubagentPrompt(
|
|
373
|
+
projectRoot: string,
|
|
374
|
+
input: SubagentInput,
|
|
375
|
+
contextKey?: string | null,
|
|
376
|
+
): string {
|
|
377
|
+
const agent = input.agent ?? "trellis-implement";
|
|
378
|
+
const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
|
|
379
|
+
const definition = readAgentDefinition(projectRoot, normalized);
|
|
380
|
+
const context = buildTrellisContext(
|
|
381
|
+
projectRoot,
|
|
382
|
+
normalized,
|
|
383
|
+
input,
|
|
384
|
+
undefined,
|
|
385
|
+
contextKey,
|
|
386
|
+
);
|
|
387
|
+
const prompt = input.prompt ?? "";
|
|
388
|
+
|
|
389
|
+
return [
|
|
390
|
+
"## Trellis Agent Definition",
|
|
391
|
+
definition || "(missing agent definition)",
|
|
392
|
+
"",
|
|
393
|
+
context,
|
|
394
|
+
"",
|
|
395
|
+
"## Delegated Task",
|
|
396
|
+
prompt,
|
|
397
|
+
].join("\n");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function runSubagent(
|
|
401
|
+
projectRoot: string,
|
|
402
|
+
input: SubagentInput,
|
|
403
|
+
contextKey?: string | null,
|
|
404
|
+
): Promise<string> {
|
|
405
|
+
const mode = input.mode ?? "single";
|
|
406
|
+
if (mode === "parallel") {
|
|
407
|
+
const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
|
|
408
|
+
const outputs = await Promise.all(
|
|
409
|
+
prompts.map((prompt) =>
|
|
410
|
+
runPi(
|
|
411
|
+
projectRoot,
|
|
412
|
+
buildSubagentPrompt(projectRoot, { ...input, prompt }, contextKey),
|
|
413
|
+
contextKey,
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
return outputs.join("\n\n---\n\n");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (mode === "chain") {
|
|
421
|
+
let previous = "";
|
|
422
|
+
const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
|
|
423
|
+
for (const prompt of prompts) {
|
|
424
|
+
previous = await runPi(
|
|
425
|
+
projectRoot,
|
|
426
|
+
buildSubagentPrompt(projectRoot, {
|
|
427
|
+
...input,
|
|
428
|
+
prompt: previous
|
|
429
|
+
? `${prompt}\n\nPrevious output:\n${previous}`
|
|
430
|
+
: prompt,
|
|
431
|
+
}, contextKey),
|
|
432
|
+
contextKey,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return previous;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return runPi(
|
|
439
|
+
projectRoot,
|
|
440
|
+
buildSubagentPrompt(projectRoot, input, contextKey),
|
|
441
|
+
contextKey,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export default function trellisExtension(pi: {
|
|
446
|
+
registerTool?: (tool: JsonObject) => void;
|
|
447
|
+
on?: (
|
|
448
|
+
event: string,
|
|
449
|
+
handler: (event: unknown, ctx?: PiExtensionContext) => unknown,
|
|
450
|
+
) => void;
|
|
451
|
+
cwd?: string;
|
|
452
|
+
}): void {
|
|
453
|
+
const projectRoot = findProjectRoot(pi.cwd ?? process.cwd());
|
|
454
|
+
const processContextKey = createProcessContextKey(projectRoot);
|
|
455
|
+
let currentContextKey: string | null = null;
|
|
456
|
+
|
|
457
|
+
const getContextKey = (input?: unknown, ctx?: PiExtensionContext): string => {
|
|
458
|
+
const contextKey = resolveContextKey(
|
|
459
|
+
input,
|
|
460
|
+
ctx,
|
|
461
|
+
currentContextKey ?? processContextKey,
|
|
462
|
+
);
|
|
463
|
+
currentContextKey = contextKey ?? processContextKey;
|
|
464
|
+
return currentContextKey;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
pi.registerTool?.({
|
|
468
|
+
name: "subagent",
|
|
469
|
+
label: "Subagent",
|
|
470
|
+
description: "Run a Trellis project sub-agent with active task context.",
|
|
471
|
+
parameters: {
|
|
472
|
+
type: "object",
|
|
473
|
+
properties: {
|
|
474
|
+
agent: {
|
|
475
|
+
type: "string",
|
|
476
|
+
description: "Agent name, such as trellis-implement or trellis-check.",
|
|
477
|
+
},
|
|
478
|
+
prompt: {
|
|
479
|
+
type: "string",
|
|
480
|
+
description: "Task prompt for the sub-agent.",
|
|
481
|
+
},
|
|
482
|
+
mode: {
|
|
483
|
+
type: "string",
|
|
484
|
+
enum: ["single", "parallel", "chain"],
|
|
485
|
+
description: "Delegation mode.",
|
|
486
|
+
},
|
|
487
|
+
prompts: {
|
|
488
|
+
type: "array",
|
|
489
|
+
items: { type: "string" },
|
|
490
|
+
description: "Prompts for parallel or chain mode.",
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
required: ["prompt"],
|
|
494
|
+
},
|
|
495
|
+
execute: async (
|
|
496
|
+
_toolCallId: string,
|
|
497
|
+
input: SubagentInput,
|
|
498
|
+
_signal?: AbortSignal,
|
|
499
|
+
_onUpdate?: (partialResult: PiToolResult) => void,
|
|
500
|
+
ctx?: PiExtensionContext,
|
|
501
|
+
): Promise<PiToolResult> => {
|
|
502
|
+
const contextKey = getContextKey(input, ctx);
|
|
503
|
+
const output = await runSubagent(projectRoot, input, contextKey);
|
|
504
|
+
return {
|
|
505
|
+
content: [{ type: "text", text: output }],
|
|
506
|
+
details: {
|
|
507
|
+
agent: input.agent ?? "trellis-implement",
|
|
508
|
+
mode: input.mode ?? "single",
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
pi.on?.("session_start", (event, ctx) => {
|
|
515
|
+
getContextKey(event, ctx);
|
|
516
|
+
ctx?.ui?.notify?.(
|
|
517
|
+
"Trellis project context is available. Use /trellis-continue to resume the current task.",
|
|
518
|
+
"info",
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
pi.on?.("before_agent_start", (event, ctx) => {
|
|
522
|
+
const contextKey = getContextKey(event, ctx);
|
|
523
|
+
const current = (event as PiBeforeAgentStartEvent).systemPrompt ?? "";
|
|
524
|
+
const context = buildTrellisContext(
|
|
525
|
+
projectRoot,
|
|
526
|
+
"trellis-implement",
|
|
527
|
+
event,
|
|
528
|
+
ctx,
|
|
529
|
+
contextKey,
|
|
530
|
+
);
|
|
531
|
+
return {
|
|
532
|
+
systemPrompt: [current, context].filter(Boolean).join("\n\n"),
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
pi.on?.("context", (event, ctx) => {
|
|
536
|
+
getContextKey(event, ctx);
|
|
537
|
+
const messages = (event as PiContextEvent).messages;
|
|
538
|
+
return Array.isArray(messages) ? { messages } : undefined;
|
|
539
|
+
});
|
|
540
|
+
pi.on?.("input", (event, ctx) => {
|
|
541
|
+
getContextKey(event, ctx);
|
|
542
|
+
return { action: "continue" };
|
|
543
|
+
});
|
|
544
|
+
pi.on?.("tool_call", (event, ctx) => {
|
|
545
|
+
const contextKey = getContextKey(event, ctx);
|
|
546
|
+
injectTrellisContextIntoBash(event, contextKey);
|
|
547
|
+
return undefined;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type AgentTemplate, type HookTemplate } from "../template-utils.js";
|
|
2
|
+
export declare function getAllAgents(): AgentTemplate[];
|
|
3
|
+
export declare function getSettingsTemplate(): HookTemplate;
|
|
4
|
+
export declare function getExtensionTemplate(): string;
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAM9B,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createTemplateReader, } from "../template-utils.js";
|
|
2
|
+
const { listMdAgents, getSettings, readTemplate } = createTemplateReader(import.meta.url);
|
|
3
|
+
export function getAllAgents() {
|
|
4
|
+
return listMdAgents();
|
|
5
|
+
}
|
|
6
|
+
export function getSettingsTemplate() {
|
|
7
|
+
return getSettings();
|
|
8
|
+
}
|
|
9
|
+
export function getExtensionTemplate() {
|
|
10
|
+
return readTemplate("extensions/trellis/index.ts.txt");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAGrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,oBAAoB,CACtE,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,CAAC;AAEF,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,CAAC,iCAAiC,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -29,7 +29,7 @@ Conversations get compacted; files don't. Every research output MUST end up as a
|
|
|
29
29
|
|
|
30
30
|
### Step 1: Resolve Current Task
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess.
|
|
33
33
|
|
|
34
34
|
Ensure `{TASK_DIR}/research/` exists:
|
|
35
35
|
|
|
@@ -11,9 +11,40 @@ 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";
|
|
16
|
+
/**
|
|
17
|
+
* Which shared hooks each platform actually invokes. Single source of truth
|
|
18
|
+
* for shared-hook distribution — both `writeSharedHooks` (runtime install)
|
|
19
|
+
* and `collectSharedHooks` (`trellis update` diff) read from this table.
|
|
20
|
+
*
|
|
21
|
+
* Routing rules encoded here:
|
|
22
|
+
* - `session-start.py` — shipped by every platform with a SessionStart
|
|
23
|
+
* hook event *except* codex + copilot, which bundle a platform-specific
|
|
24
|
+
* session-start.py under their own template dirs.
|
|
25
|
+
* - `inject-workflow-state.py` — every platform with a UserPromptSubmit
|
|
26
|
+
* (or equivalent) event. Kiro + codex self-included; platforms without
|
|
27
|
+
* per-turn main-session hooks are excluded.
|
|
28
|
+
* - `inject-subagent-context.py` — class-1 (push-based) platforms only.
|
|
29
|
+
* Class-2 (pull-based) platforms (codex, copilot, gemini, qoder) can't
|
|
30
|
+
* have hooks mutate sub-agent prompts — their sub-agents load context
|
|
31
|
+
* via a prelude instead.
|
|
32
|
+
* - Kiro supports only `agentSpawn` (no SessionStart / UserPromptSubmit
|
|
33
|
+
* event), so it takes just `inject-subagent-context.py`.
|
|
34
|
+
* - Claude Code `statusLine` is intentionally not installed by default.
|
|
35
|
+
* Users can add their own statusLine command in `.claude/settings.json`
|
|
36
|
+
* without Trellis owning a generated hook file.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SHARED_HOOKS_BY_PLATFORM: Record<SharedHookPlatform, readonly SharedHookName[]>;
|
|
14
39
|
/**
|
|
15
40
|
* Get all shared hook scripts. Content is platform-independent and can be
|
|
16
41
|
* written directly without placeholder resolution.
|
|
17
42
|
*/
|
|
18
43
|
export declare function getSharedHookScripts(): HookScript[];
|
|
44
|
+
/**
|
|
45
|
+
* Get the shared hook scripts that a given platform actually registers.
|
|
46
|
+
* Drives both `writeSharedHooks` and `collectSharedHooks` so distribution
|
|
47
|
+
* never drifts from the per-platform capability declared above.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getSharedHookScriptsForPlatform(platform: SharedHookPlatform): HookScript[];
|
|
19
50
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD"}
|
|
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;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CA4B1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
|