@trailstep/sub-agents 0.2.0
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/LICENSE +176 -0
- package/README.md +175 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +1062 -0
- package/dist/index.js.map +1 -0
- package/package.json +238 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
// src/delegate/workflow.ts
|
|
2
|
+
import { defineWorkflow } from "@trailstep/authoring";
|
|
3
|
+
|
|
4
|
+
// src/delegate/schema.ts
|
|
5
|
+
import { jsonSchema } from "@trailstep/authoring";
|
|
6
|
+
var delegateArtifactShape = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
name: { type: "string" },
|
|
10
|
+
path: { type: "string" },
|
|
11
|
+
mediaType: { type: "string" }
|
|
12
|
+
},
|
|
13
|
+
required: ["name", "path"],
|
|
14
|
+
additionalProperties: false
|
|
15
|
+
};
|
|
16
|
+
var delegateInputShape = jsonSchema({
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
task: { type: "string" },
|
|
20
|
+
context: { type: "string" },
|
|
21
|
+
mode: { type: "string", enum: ["explore", "implement", "review", "general"] },
|
|
22
|
+
cwd: { type: "string" },
|
|
23
|
+
maxTurns: { type: "number" },
|
|
24
|
+
summarize: { type: "boolean" },
|
|
25
|
+
worktree: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
enabled: { type: "boolean" },
|
|
29
|
+
path: { type: "string" },
|
|
30
|
+
baseRef: { type: "string" },
|
|
31
|
+
baseBranch: { type: "string" },
|
|
32
|
+
branch: { type: "string" },
|
|
33
|
+
cleanup: { type: "string", enum: ["auto", "never", "always"] },
|
|
34
|
+
forceCleanup: { type: "boolean" }
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ["task"],
|
|
40
|
+
additionalProperties: false
|
|
41
|
+
});
|
|
42
|
+
var delegateOutputShape = jsonSchema({
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
status: { type: "string", enum: ["completed", "blocked", "cancelled"] },
|
|
46
|
+
summary: { type: "string" },
|
|
47
|
+
result: { type: "string" },
|
|
48
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
49
|
+
artifacts: { type: "array", items: delegateArtifactShape },
|
|
50
|
+
worktreePath: { type: "string" },
|
|
51
|
+
worktreeBranch: { type: "string" },
|
|
52
|
+
worktreeCleanup: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
requested: { type: "string", enum: ["auto", "never", "always"] },
|
|
56
|
+
status: {
|
|
57
|
+
type: "string",
|
|
58
|
+
enum: ["not-managed", "kept", "removed", "kept-dirty", "failed"]
|
|
59
|
+
},
|
|
60
|
+
reason: { type: "string" }
|
|
61
|
+
},
|
|
62
|
+
required: ["requested", "status"],
|
|
63
|
+
additionalProperties: false
|
|
64
|
+
},
|
|
65
|
+
questionsAsked: { type: "number" },
|
|
66
|
+
turns: { type: "number" }
|
|
67
|
+
},
|
|
68
|
+
required: ["status", "summary", "questionsAsked", "turns"],
|
|
69
|
+
additionalProperties: false
|
|
70
|
+
});
|
|
71
|
+
var delegateTurnOutputShape = jsonSchema({
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
status: { type: "string", enum: ["completed", "continue", "question", "blocked"] },
|
|
75
|
+
summary: { type: "string" },
|
|
76
|
+
memoryPatch: { type: "string" },
|
|
77
|
+
question: { type: "string" },
|
|
78
|
+
result: { type: "string" },
|
|
79
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
80
|
+
artifacts: { type: "array", items: delegateArtifactShape }
|
|
81
|
+
},
|
|
82
|
+
required: ["status", "summary"],
|
|
83
|
+
additionalProperties: false
|
|
84
|
+
});
|
|
85
|
+
var parentAnswerShape = jsonSchema({
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
answer: { type: "string" }
|
|
89
|
+
},
|
|
90
|
+
required: ["answer"],
|
|
91
|
+
additionalProperties: false
|
|
92
|
+
});
|
|
93
|
+
function normalizeDelegateInput(input, defaults = {}) {
|
|
94
|
+
const mode = input.mode ?? defaults.mode ?? "general";
|
|
95
|
+
return {
|
|
96
|
+
task: input.task,
|
|
97
|
+
...nonEmptyOptional(input.context) === void 0 ? {} : { context: nonEmptyOptional(input.context) },
|
|
98
|
+
mode,
|
|
99
|
+
...nonEmptyOptional(input.cwd) === void 0 ? {} : { cwd: nonEmptyOptional(input.cwd) },
|
|
100
|
+
maxTurns: normalizeMaxTurns(input.maxTurns ?? defaults.maxTurns),
|
|
101
|
+
summarize: input.summarize ?? defaults.summarize ?? (mode === "explore" || mode === "general"),
|
|
102
|
+
worktree: {
|
|
103
|
+
enabled: input.worktree?.enabled ?? false,
|
|
104
|
+
...nonEmptyOptional(input.worktree?.path) === void 0 ? {} : { path: nonEmptyOptional(input.worktree?.path) },
|
|
105
|
+
...nonEmptyOptional(input.worktree?.baseRef ?? input.worktree?.baseBranch) === void 0 ? {} : { baseRef: nonEmptyOptional(input.worktree?.baseRef ?? input.worktree?.baseBranch) },
|
|
106
|
+
...nonEmptyOptional(input.worktree?.baseBranch) === void 0 ? {} : { baseBranch: nonEmptyOptional(input.worktree?.baseBranch) },
|
|
107
|
+
...nonEmptyOptional(input.worktree?.branch) === void 0 ? {} : { branch: nonEmptyOptional(input.worktree?.branch) },
|
|
108
|
+
cleanup: input.worktree?.cleanup ?? "auto",
|
|
109
|
+
forceCleanup: input.worktree?.forceCleanup ?? false
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizeMaxTurns(value) {
|
|
114
|
+
if (value === void 0) {
|
|
115
|
+
return 10;
|
|
116
|
+
}
|
|
117
|
+
if (!Number.isFinite(value)) {
|
|
118
|
+
return 10;
|
|
119
|
+
}
|
|
120
|
+
return Math.max(1, Math.floor(value));
|
|
121
|
+
}
|
|
122
|
+
function nonEmptyOptional(value) {
|
|
123
|
+
const trimmed = value?.trim();
|
|
124
|
+
return trimmed && trimmed.length > 0 ? trimmed : void 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/delegate/steps.ts
|
|
128
|
+
import { execFile } from "child_process";
|
|
129
|
+
import { mkdir } from "fs/promises";
|
|
130
|
+
import { dirname, resolve } from "path";
|
|
131
|
+
import { promisify } from "util";
|
|
132
|
+
import { done, notify, state, step } from "@trailstep/authoring";
|
|
133
|
+
|
|
134
|
+
// src/delegate/prompts.ts
|
|
135
|
+
import { list, promptSections, section } from "@trailstep/authoring";
|
|
136
|
+
function delegateTurnPrompt({ input }) {
|
|
137
|
+
return promptSections(
|
|
138
|
+
section(
|
|
139
|
+
"Role",
|
|
140
|
+
"You are a continued delegate agent working inside one TrailStep run. Preserve continuity from the run-local memory below, but do not assume or use memory from any separate run."
|
|
141
|
+
),
|
|
142
|
+
section("Original task", input.task),
|
|
143
|
+
section("Original context", input.context),
|
|
144
|
+
section("Mode", `${input.mode}: ${modeGuidance(input.mode)}`),
|
|
145
|
+
section(
|
|
146
|
+
"Execution location",
|
|
147
|
+
[
|
|
148
|
+
`CWD/worktree path: ${input.worktreePath ?? input.executionCwd}`,
|
|
149
|
+
"Honor this directory for any project inspection or file edits."
|
|
150
|
+
].join("\n")
|
|
151
|
+
),
|
|
152
|
+
section("Accumulated run-local memory summary", input.memory || "No prior delegate memory."),
|
|
153
|
+
section("Previous questions and parent/human answers", formatQuestionsAndAnswers(input)),
|
|
154
|
+
section("Latest summary", input.latestSummary ?? "No prior summary."),
|
|
155
|
+
section(
|
|
156
|
+
"Current turn",
|
|
157
|
+
[
|
|
158
|
+
`Turn: ${input.turn} of ${input.maxTurns}`,
|
|
159
|
+
`Summarize mode: ${String(input.summarize)}`
|
|
160
|
+
].join("\n")
|
|
161
|
+
),
|
|
162
|
+
section(
|
|
163
|
+
"Instructions",
|
|
164
|
+
list([
|
|
165
|
+
"Do one focused turn of delegated work; keep the work bounded and observable.",
|
|
166
|
+
"Use the run-local memory summary and previous Q/A as continuity, not an unlimited transcript.",
|
|
167
|
+
"If you can finish the task now, return status `completed` with a concise summary and final result.",
|
|
168
|
+
"If more delegated work is needed and no parent answer is required, return status `continue` and a memory patch for the next turn.",
|
|
169
|
+
"If blocked on a parent/human decision, return status `question`, include exactly one clear question, and summarize why it is needed.",
|
|
170
|
+
"If the task cannot proceed safely, return status `blocked` with the reason in the summary/result.",
|
|
171
|
+
"When you learn durable facts for this run, include a compact `memoryPatch`; do not copy large transcripts, files, or diffs.",
|
|
172
|
+
"Report changed files and artifacts only when you actually changed or produced them."
|
|
173
|
+
])
|
|
174
|
+
),
|
|
175
|
+
section("Exact output contract", outputContract)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function modeGuidance(mode) {
|
|
179
|
+
switch (mode) {
|
|
180
|
+
case "explore":
|
|
181
|
+
return "investigate, summarize findings, and avoid code changes unless explicitly needed.";
|
|
182
|
+
case "implement":
|
|
183
|
+
return "make the smallest safe implementation progress and report changed files.";
|
|
184
|
+
case "review":
|
|
185
|
+
return "review existing work, identify issues, and avoid unnecessary edits.";
|
|
186
|
+
case "general":
|
|
187
|
+
return "use judgment to complete the delegated task with focused, bounded work.";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function formatQuestionsAndAnswers(input) {
|
|
191
|
+
if (input.questionsAndAnswers.length === 0) {
|
|
192
|
+
return "No parent/human questions have been asked in this run.";
|
|
193
|
+
}
|
|
194
|
+
return input.questionsAndAnswers.map(
|
|
195
|
+
(item, index) => [`${index + 1}. Question: ${item.question}`, ` Answer: ${item.answer}`].join("\n")
|
|
196
|
+
).join("\n");
|
|
197
|
+
}
|
|
198
|
+
var outputContract = `Return the structured JSON object requested by the wrapper.
|
|
199
|
+
|
|
200
|
+
Status meanings:
|
|
201
|
+
- completed: the delegated task is finished; include result when useful.
|
|
202
|
+
- continue: more bounded work is needed in another turn; include memoryPatch for durable continuity.
|
|
203
|
+
- question: parent/human input is required; include exactly one clear question.
|
|
204
|
+
- blocked: the task cannot proceed safely; summarize why and include result details when useful.
|
|
205
|
+
|
|
206
|
+
Only report changedFiles/artifacts that you actually changed or produced.`;
|
|
207
|
+
|
|
208
|
+
// src/delegate/steps.ts
|
|
209
|
+
var MEMORY_LIMIT = 12e3;
|
|
210
|
+
var execFileAsync = promisify(execFile);
|
|
211
|
+
var DELEGATE_STATE_KEYS = {
|
|
212
|
+
initialized: "delegate.initialized",
|
|
213
|
+
input: "delegate.input",
|
|
214
|
+
memory: "delegate.memory",
|
|
215
|
+
turns: "delegate.turns",
|
|
216
|
+
questions: "delegate.questions",
|
|
217
|
+
answers: "delegate.answers",
|
|
218
|
+
latestSummary: "delegate.latestSummary",
|
|
219
|
+
artifacts: "delegate.artifacts",
|
|
220
|
+
changedFiles: "delegate.changedFiles",
|
|
221
|
+
result: "delegate.result",
|
|
222
|
+
worktreePath: "delegate.worktreePath",
|
|
223
|
+
worktreeBranch: "delegate.worktreeBranch",
|
|
224
|
+
worktreeCleanup: "delegate.worktreeCleanup"
|
|
225
|
+
};
|
|
226
|
+
function initializeDelegateStep(input, defaults = {}) {
|
|
227
|
+
return initializeDelegateStepFactory({ input, defaults });
|
|
228
|
+
}
|
|
229
|
+
var initializeDelegateStepFactory = step({
|
|
230
|
+
id: "initialize-delegate",
|
|
231
|
+
title: "Initialize delegate"
|
|
232
|
+
}).display(({ input }) => `Delegating: ${input.input.task}`).do(async ({ input, defaults }) => {
|
|
233
|
+
const initialized = await state.get(DELEGATE_STATE_KEYS.initialized) ?? false;
|
|
234
|
+
if (initialized) {
|
|
235
|
+
return prepareDelegateTurnStep({ turn: 1 });
|
|
236
|
+
}
|
|
237
|
+
const normalized = normalizeDelegateInput(input, defaults);
|
|
238
|
+
if (normalized.cwd !== void 0 && normalized.worktree.enabled) {
|
|
239
|
+
return await finalDone({
|
|
240
|
+
status: "blocked",
|
|
241
|
+
summary: "Delegate input cannot specify both cwd and worktree.enabled. Choose an existing cwd or managed worktree creation, not both.",
|
|
242
|
+
questionsAsked: 0,
|
|
243
|
+
turns: 0
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const managedWorktree = normalized.worktree.enabled ? await createManagedWorktree(normalized) : void 0;
|
|
247
|
+
const executionCwd = managedWorktree?.path ?? resolveDelegateCwd(normalized.cwd);
|
|
248
|
+
const normalizedInput = {
|
|
249
|
+
...normalized,
|
|
250
|
+
...normalized.cwd !== void 0 || managedWorktree !== void 0 ? { cwd: executionCwd } : {}
|
|
251
|
+
};
|
|
252
|
+
await state.set(DELEGATE_STATE_KEYS.initialized, true);
|
|
253
|
+
await state.set(DELEGATE_STATE_KEYS.input, normalizedInput);
|
|
254
|
+
await state.set(DELEGATE_STATE_KEYS.memory, "");
|
|
255
|
+
await state.set(DELEGATE_STATE_KEYS.turns, 0);
|
|
256
|
+
await state.set(DELEGATE_STATE_KEYS.questions, 0);
|
|
257
|
+
await state.set(DELEGATE_STATE_KEYS.answers, []);
|
|
258
|
+
await state.set(DELEGATE_STATE_KEYS.latestSummary, null);
|
|
259
|
+
await state.set(DELEGATE_STATE_KEYS.artifacts, []);
|
|
260
|
+
await state.set(DELEGATE_STATE_KEYS.changedFiles, []);
|
|
261
|
+
await state.set(DELEGATE_STATE_KEYS.result, null);
|
|
262
|
+
await state.set(DELEGATE_STATE_KEYS.worktreePath, managedWorktree?.path ?? null);
|
|
263
|
+
await state.set(DELEGATE_STATE_KEYS.worktreeBranch, managedWorktree?.branch ?? null);
|
|
264
|
+
await state.set(DELEGATE_STATE_KEYS.worktreeCleanup, null);
|
|
265
|
+
await notify.progress("Initialized delegate memory", {
|
|
266
|
+
mode: normalizedInput.mode,
|
|
267
|
+
maxTurns: normalizedInput.maxTurns,
|
|
268
|
+
cwd: executionCwd,
|
|
269
|
+
...managedWorktree === void 0 ? {} : { worktreePath: managedWorktree.path, worktreeBranch: managedWorktree.branch }
|
|
270
|
+
});
|
|
271
|
+
return prepareDelegateTurnStep({ turn: 1 });
|
|
272
|
+
});
|
|
273
|
+
var prepareDelegateTurnStep = step({
|
|
274
|
+
id: "prepare-delegate-turn",
|
|
275
|
+
title: "Prepare delegate turn"
|
|
276
|
+
}).do(async (turnRequest) => {
|
|
277
|
+
const input = await requiredState(DELEGATE_STATE_KEYS.input);
|
|
278
|
+
const turns = await state.get(DELEGATE_STATE_KEYS.turns) ?? 0;
|
|
279
|
+
const questionsAsked = await state.get(DELEGATE_STATE_KEYS.questions) ?? 0;
|
|
280
|
+
if (turnRequest.turn > input.maxTurns) {
|
|
281
|
+
await notify.warning("Delegate exceeded maxTurns", { maxTurns: input.maxTurns, turns });
|
|
282
|
+
return await finalDone({
|
|
283
|
+
status: "blocked",
|
|
284
|
+
summary: "Delegate exceeded maxTurns without completing.",
|
|
285
|
+
...await finalOptionalOutputFields(),
|
|
286
|
+
questionsAsked,
|
|
287
|
+
turns
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const executionCwd = input.cwd ?? state.executionCwd ?? state.cwd ?? process.cwd();
|
|
291
|
+
const turnInput = {
|
|
292
|
+
task: input.task,
|
|
293
|
+
...input.context === void 0 ? {} : { context: input.context },
|
|
294
|
+
mode: input.mode,
|
|
295
|
+
executionCwd,
|
|
296
|
+
...input.worktree.enabled ? { worktreePath: executionCwd } : {},
|
|
297
|
+
memory: await state.get(DELEGATE_STATE_KEYS.memory) ?? "",
|
|
298
|
+
questionsAndAnswers: await state.get(DELEGATE_STATE_KEYS.answers) ?? [],
|
|
299
|
+
latestSummary: await state.get(DELEGATE_STATE_KEYS.latestSummary) ?? void 0,
|
|
300
|
+
turn: turnRequest.turn,
|
|
301
|
+
maxTurns: input.maxTurns,
|
|
302
|
+
summarize: input.summarize
|
|
303
|
+
};
|
|
304
|
+
return delegateTurnStep(turnInput);
|
|
305
|
+
});
|
|
306
|
+
var delegateTurnStep = step({
|
|
307
|
+
id: "delegate-turn",
|
|
308
|
+
title: "Delegate turn",
|
|
309
|
+
cwd: ({ input }) => input.executionCwd
|
|
310
|
+
}).display(({ input }) => `Delegating: ${input.task}`).prompt(delegateTurnPrompt, {
|
|
311
|
+
agent: "delegateAgent",
|
|
312
|
+
output: delegateTurnOutputShape
|
|
313
|
+
}).display(({ output }) => output.summary).do(async (turnOutput, input) => {
|
|
314
|
+
const storedTurns = await state.get(DELEGATE_STATE_KEYS.turns) ?? 0;
|
|
315
|
+
const alreadyProcessed = storedTurns >= input.turn;
|
|
316
|
+
const turns = alreadyProcessed ? storedTurns : storedTurns + 1;
|
|
317
|
+
if (!alreadyProcessed) {
|
|
318
|
+
await state.set(DELEGATE_STATE_KEYS.turns, turns);
|
|
319
|
+
await state.set(DELEGATE_STATE_KEYS.latestSummary, turnOutput.summary);
|
|
320
|
+
await appendMemory(turns, turnOutput);
|
|
321
|
+
await mergeChangedFiles(turnOutput.changedFiles ?? []);
|
|
322
|
+
await mergeArtifacts(turnOutput.artifacts ?? []);
|
|
323
|
+
await state.set(DELEGATE_STATE_KEYS.result, turnOutput.result ?? null);
|
|
324
|
+
await notify.progress("Updated delegate memory", { turn: turns });
|
|
325
|
+
for (const artifact of turnOutput.artifacts ?? []) {
|
|
326
|
+
await notify.artifact(artifact.name, {
|
|
327
|
+
path: artifact.path,
|
|
328
|
+
...artifact.mediaType === void 0 ? {} : { mediaType: artifact.mediaType }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (turnOutput.status === "completed") {
|
|
333
|
+
const questionsAsked = await state.get(DELEGATE_STATE_KEYS.questions) ?? 0;
|
|
334
|
+
return await finalDone({
|
|
335
|
+
status: "completed",
|
|
336
|
+
summary: turnOutput.summary,
|
|
337
|
+
...await finalOptionalOutputFields(turnOutput),
|
|
338
|
+
questionsAsked,
|
|
339
|
+
turns
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (turnOutput.status === "blocked") {
|
|
343
|
+
const questionsAsked = await state.get(DELEGATE_STATE_KEYS.questions) ?? 0;
|
|
344
|
+
return await finalDone({
|
|
345
|
+
status: "blocked",
|
|
346
|
+
summary: turnOutput.summary,
|
|
347
|
+
...await finalOptionalOutputFields(turnOutput),
|
|
348
|
+
questionsAsked,
|
|
349
|
+
turns
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (turnOutput.status === "question") {
|
|
353
|
+
const question = turnOutput.question?.trim();
|
|
354
|
+
if (!question) {
|
|
355
|
+
const questionsAsked2 = await state.get(DELEGATE_STATE_KEYS.questions) ?? 0;
|
|
356
|
+
return await finalDone({
|
|
357
|
+
status: "blocked",
|
|
358
|
+
summary: "Delegate requested a parent answer but did not provide a question.",
|
|
359
|
+
...await finalOptionalOutputFields(turnOutput),
|
|
360
|
+
questionsAsked: questionsAsked2,
|
|
361
|
+
turns
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const storedQuestions = await state.get(DELEGATE_STATE_KEYS.questions) ?? 0;
|
|
365
|
+
const questionsAsked = alreadyProcessed ? storedQuestions : storedQuestions + 1;
|
|
366
|
+
if (!alreadyProcessed) {
|
|
367
|
+
await state.set(DELEGATE_STATE_KEYS.questions, questionsAsked);
|
|
368
|
+
await notify.progress("Delegate asked parent question", {
|
|
369
|
+
turn: input.turn,
|
|
370
|
+
questionsAsked
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return askParentStep({ question, nextTurn: input.turn + 1 });
|
|
374
|
+
}
|
|
375
|
+
return prepareDelegateTurnStep({ turn: input.turn + 1 });
|
|
376
|
+
});
|
|
377
|
+
var askParentStep = step({
|
|
378
|
+
id: "ask-parent",
|
|
379
|
+
title: "Ask parent"
|
|
380
|
+
}).display(({ input }) => input.question).wait(({ input }) => ({
|
|
381
|
+
id: "parent-answer",
|
|
382
|
+
kind: "input",
|
|
383
|
+
message: input.question,
|
|
384
|
+
output: parentAnswerShape
|
|
385
|
+
})).do(async (context, input) => {
|
|
386
|
+
const waitContext = context;
|
|
387
|
+
const answerOutput = waitContext.waits?.["parent-answer"];
|
|
388
|
+
const answer = answerOutput?.answer ?? "";
|
|
389
|
+
const answers = await state.get(DELEGATE_STATE_KEYS.answers) ?? [];
|
|
390
|
+
const alreadyRecorded = answers.some(
|
|
391
|
+
(item) => item.question === input.question && item.answer === answer
|
|
392
|
+
);
|
|
393
|
+
if (answerOutput !== void 0 && !alreadyRecorded) {
|
|
394
|
+
await state.set(DELEGATE_STATE_KEYS.answers, [
|
|
395
|
+
...answers,
|
|
396
|
+
{ question: input.question, answer }
|
|
397
|
+
]);
|
|
398
|
+
await appendMemoryNote(`Parent answered: ${answer}`);
|
|
399
|
+
await notify.progress("Recorded parent answer", { questionsAnswered: answers.length + 1 });
|
|
400
|
+
}
|
|
401
|
+
return prepareDelegateTurnStep({ turn: input.nextTurn });
|
|
402
|
+
});
|
|
403
|
+
async function createManagedWorktree(input) {
|
|
404
|
+
const base = state.projectCwd ?? state.executionCwd ?? state.cwd ?? process.cwd();
|
|
405
|
+
const safeRunName = sanitizeRefSegment(state.name);
|
|
406
|
+
const worktreePath = resolve(base, input.worktree.path ?? `.trailstep/worktrees/${safeRunName}`);
|
|
407
|
+
const branch = input.worktree.branch ?? `trailstep/delegate/${safeRunName}`;
|
|
408
|
+
const baseRef = input.worktree.baseRef ?? input.worktree.baseBranch ?? "HEAD";
|
|
409
|
+
await mkdir(dirname(worktreePath), { recursive: true });
|
|
410
|
+
await git(base, ["worktree", "add", "-b", branch, worktreePath, baseRef]);
|
|
411
|
+
await notify.progress("Created delegate worktree", { path: worktreePath, branch, baseRef });
|
|
412
|
+
return { path: worktreePath, branch };
|
|
413
|
+
}
|
|
414
|
+
async function cleanupManagedWorktree(finalStatus) {
|
|
415
|
+
const input = await state.get(DELEGATE_STATE_KEYS.input);
|
|
416
|
+
const worktreePath = await state.get(DELEGATE_STATE_KEYS.worktreePath);
|
|
417
|
+
const worktreeBranch = await state.get(DELEGATE_STATE_KEYS.worktreeBranch);
|
|
418
|
+
if (input?.worktree.enabled !== true || worktreePath === void 0 || worktreePath === null) {
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
const requested = input.worktree.cleanup;
|
|
422
|
+
const keep = async (status, reason) => {
|
|
423
|
+
const details2 = { requested, status, reason };
|
|
424
|
+
await state.set(DELEGATE_STATE_KEYS.worktreeCleanup, details2);
|
|
425
|
+
await notify.progress("Kept delegate worktree", { path: worktreePath, reason });
|
|
426
|
+
return details2;
|
|
427
|
+
};
|
|
428
|
+
if (requested === "never") {
|
|
429
|
+
return keep("kept", "cleanup=never");
|
|
430
|
+
}
|
|
431
|
+
if (requested === "auto" && finalStatus !== "completed") {
|
|
432
|
+
return keep("kept", "auto cleanup keeps worktree because delegate did not complete");
|
|
433
|
+
}
|
|
434
|
+
let clean = false;
|
|
435
|
+
try {
|
|
436
|
+
clean = (await git(worktreePath, ["status", "--porcelain"])).trim().length === 0;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return keep("failed", `could not prove worktree cleanliness: ${errorMessage(error)}`);
|
|
439
|
+
}
|
|
440
|
+
if (!clean && !input.worktree.forceCleanup) {
|
|
441
|
+
return keep("kept-dirty", "worktree has changes and forceCleanup is false");
|
|
442
|
+
}
|
|
443
|
+
const repoCwd = state.projectCwd ?? state.executionCwd ?? state.cwd ?? process.cwd();
|
|
444
|
+
try {
|
|
445
|
+
const args = [
|
|
446
|
+
"worktree",
|
|
447
|
+
"remove",
|
|
448
|
+
...input.worktree.forceCleanup ? ["--force"] : [],
|
|
449
|
+
worktreePath
|
|
450
|
+
];
|
|
451
|
+
await git(repoCwd, args);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return keep("failed", `git worktree remove failed: ${errorMessage(error)}`);
|
|
454
|
+
}
|
|
455
|
+
const branchCleanupReason = await cleanupManagedWorktreeBranch({
|
|
456
|
+
repoCwd,
|
|
457
|
+
branch: worktreeBranch,
|
|
458
|
+
createdByRun: input.worktree.branch === void 0
|
|
459
|
+
});
|
|
460
|
+
const details = {
|
|
461
|
+
requested,
|
|
462
|
+
status: branchCleanupReason?.startsWith("managed branch cleanup failed") ? "failed" : "removed",
|
|
463
|
+
...branchCleanupReason === void 0 ? {} : { reason: branchCleanupReason }
|
|
464
|
+
};
|
|
465
|
+
await state.set(DELEGATE_STATE_KEYS.worktreeCleanup, details);
|
|
466
|
+
await notify.progress("Removed delegate worktree", {
|
|
467
|
+
path: worktreePath,
|
|
468
|
+
reason: branchCleanupReason
|
|
469
|
+
});
|
|
470
|
+
return details;
|
|
471
|
+
}
|
|
472
|
+
async function cleanupManagedWorktreeBranch({
|
|
473
|
+
repoCwd,
|
|
474
|
+
branch,
|
|
475
|
+
createdByRun
|
|
476
|
+
}) {
|
|
477
|
+
if (branch === void 0 || branch === null) {
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
if (!createdByRun) {
|
|
481
|
+
return "user-supplied branch retained";
|
|
482
|
+
}
|
|
483
|
+
let currentBranch;
|
|
484
|
+
try {
|
|
485
|
+
currentBranch = (await git(repoCwd, ["branch", "--show-current"])).trim();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
return `managed branch cleanup failed after worktree removal: could not determine current branch: ${errorMessage(error)}`;
|
|
488
|
+
}
|
|
489
|
+
if (currentBranch === branch) {
|
|
490
|
+
return "managed branch retained because it is currently checked out";
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await git(repoCwd, ["branch", "-d", branch]);
|
|
494
|
+
return void 0;
|
|
495
|
+
} catch (error) {
|
|
496
|
+
return `managed branch cleanup failed after worktree removal: ${errorMessage(error)}`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function git(cwd, args) {
|
|
500
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
501
|
+
return stdout;
|
|
502
|
+
}
|
|
503
|
+
function resolveDelegateCwd(cwd) {
|
|
504
|
+
const base = state.projectCwd ?? state.executionCwd ?? state.cwd ?? process.cwd();
|
|
505
|
+
return cwd === void 0 ? base : resolve(base, cwd);
|
|
506
|
+
}
|
|
507
|
+
function sanitizeRefSegment(value) {
|
|
508
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
|
|
509
|
+
}
|
|
510
|
+
function errorMessage(error) {
|
|
511
|
+
return error instanceof Error ? error.message : String(error);
|
|
512
|
+
}
|
|
513
|
+
async function appendMemory(turn, output) {
|
|
514
|
+
const lines = [`Turn ${turn}: ${output.summary}`];
|
|
515
|
+
if (output.memoryPatch?.trim()) {
|
|
516
|
+
lines.push(`Memory patch: ${output.memoryPatch.trim()}`);
|
|
517
|
+
}
|
|
518
|
+
await appendMemoryNote(lines.join("\n"));
|
|
519
|
+
}
|
|
520
|
+
async function appendMemoryNote(note) {
|
|
521
|
+
const existing = await state.get(DELEGATE_STATE_KEYS.memory) ?? "";
|
|
522
|
+
const updated = [existing.trim(), note.trim()].filter(Boolean).join("\n\n");
|
|
523
|
+
await state.set(DELEGATE_STATE_KEYS.memory, trimRollingMemory(updated));
|
|
524
|
+
}
|
|
525
|
+
function trimRollingMemory(memory) {
|
|
526
|
+
if (memory.length <= MEMORY_LIMIT) {
|
|
527
|
+
return memory;
|
|
528
|
+
}
|
|
529
|
+
return `[older delegate memory trimmed]
|
|
530
|
+
${memory.slice(-MEMORY_LIMIT)}`;
|
|
531
|
+
}
|
|
532
|
+
async function mergeChangedFiles(changedFiles) {
|
|
533
|
+
if (changedFiles.length === 0) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const existing = await state.get(DELEGATE_STATE_KEYS.changedFiles) ?? [];
|
|
537
|
+
await state.set(DELEGATE_STATE_KEYS.changedFiles, uniqueStrings([...existing, ...changedFiles]));
|
|
538
|
+
}
|
|
539
|
+
async function mergeArtifacts(artifacts) {
|
|
540
|
+
if (artifacts.length === 0) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const existing = await state.get(DELEGATE_STATE_KEYS.artifacts) ?? [];
|
|
544
|
+
const byPath = new Map(existing.map((artifact) => [artifact.path, artifact]));
|
|
545
|
+
for (const artifact of artifacts) {
|
|
546
|
+
byPath.set(artifact.path, artifact);
|
|
547
|
+
}
|
|
548
|
+
await state.set(DELEGATE_STATE_KEYS.artifacts, [...byPath.values()]);
|
|
549
|
+
}
|
|
550
|
+
async function finalOptionalOutputFields(latest) {
|
|
551
|
+
const result = latest?.result ?? await state.get(DELEGATE_STATE_KEYS.result);
|
|
552
|
+
const changedFiles = await state.get(DELEGATE_STATE_KEYS.changedFiles) ?? [];
|
|
553
|
+
const artifacts = await state.get(DELEGATE_STATE_KEYS.artifacts) ?? [];
|
|
554
|
+
const worktreePath = await state.get(DELEGATE_STATE_KEYS.worktreePath);
|
|
555
|
+
const worktreeBranch = await state.get(DELEGATE_STATE_KEYS.worktreeBranch);
|
|
556
|
+
const worktreeCleanup = await state.get(
|
|
557
|
+
DELEGATE_STATE_KEYS.worktreeCleanup
|
|
558
|
+
);
|
|
559
|
+
return {
|
|
560
|
+
...result === void 0 || result === null ? {} : { result },
|
|
561
|
+
...changedFiles.length === 0 ? {} : { changedFiles },
|
|
562
|
+
...artifacts.length === 0 ? {} : { artifacts },
|
|
563
|
+
...worktreePath === void 0 || worktreePath === null ? {} : { worktreePath },
|
|
564
|
+
...worktreeBranch === void 0 || worktreeBranch === null ? {} : { worktreeBranch },
|
|
565
|
+
...worktreeCleanup === void 0 || worktreeCleanup === null ? {} : { worktreeCleanup }
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function finalDone(output) {
|
|
569
|
+
const cleanup = await cleanupManagedWorktree(output.status);
|
|
570
|
+
const finalOutput = cleanup === void 0 ? output : { ...output, worktreeCleanup: cleanup };
|
|
571
|
+
return done(finalOutput, { message: delegateTerminalMessage(finalOutput) });
|
|
572
|
+
}
|
|
573
|
+
function delegateTerminalMessage(output) {
|
|
574
|
+
const lines = [
|
|
575
|
+
`Delegate ${output.status}: ${output.summary}`,
|
|
576
|
+
`Turns: ${output.turns}; questions asked: ${output.questionsAsked}`
|
|
577
|
+
];
|
|
578
|
+
if (output.changedFiles && output.changedFiles.length > 0) {
|
|
579
|
+
lines.push(`Changed files: ${output.changedFiles.join(", ")}`);
|
|
580
|
+
}
|
|
581
|
+
if (output.worktreePath) {
|
|
582
|
+
lines.push(`Worktree: ${output.worktreePath}`);
|
|
583
|
+
}
|
|
584
|
+
if (output.worktreeBranch) {
|
|
585
|
+
lines.push(`Worktree branch: ${output.worktreeBranch}`);
|
|
586
|
+
}
|
|
587
|
+
if (output.worktreeCleanup) {
|
|
588
|
+
lines.push(
|
|
589
|
+
`Worktree cleanup: ${output.worktreeCleanup.status} (${output.worktreeCleanup.requested})${output.worktreeCleanup.reason ? ` - ${output.worktreeCleanup.reason}` : ""}`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
if (output.result?.trim()) {
|
|
593
|
+
lines.push(output.result.trim());
|
|
594
|
+
}
|
|
595
|
+
return lines.join("\n");
|
|
596
|
+
}
|
|
597
|
+
async function requiredState(key) {
|
|
598
|
+
const value = await state.get(key);
|
|
599
|
+
if (value === void 0 || value === null) {
|
|
600
|
+
throw new Error(`Delegate state is missing required key: ${key}`);
|
|
601
|
+
}
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function uniqueStrings(values) {
|
|
605
|
+
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/delegate/workflow.ts
|
|
609
|
+
function defineDelegateWorkflow(config) {
|
|
610
|
+
return defineWorkflow({
|
|
611
|
+
id: config.id,
|
|
612
|
+
description: config.description,
|
|
613
|
+
skill: delegateSkill(config),
|
|
614
|
+
inputShape: delegateInputShape,
|
|
615
|
+
outputShape: delegateOutputShape,
|
|
616
|
+
agents: {
|
|
617
|
+
delegateAgent: {
|
|
618
|
+
size: config.agentSize ?? "medium",
|
|
619
|
+
thinking: config.agentThinking ?? "medium",
|
|
620
|
+
description: config.description
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
start(input) {
|
|
624
|
+
return initializeDelegateStep(input, {
|
|
625
|
+
mode: config.defaultMode,
|
|
626
|
+
maxTurns: config.defaultMaxTurns,
|
|
627
|
+
...config.summarize === void 0 ? {} : { summarize: config.summarize }
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
var delegate = defineDelegateWorkflow({
|
|
633
|
+
id: "delegate",
|
|
634
|
+
description: "Runs delegated focused work while preserving run-local continuity.",
|
|
635
|
+
skillName: "trst-delegate",
|
|
636
|
+
skillDescription: "Use as a flexible sub-agent for focused TrailStep delegate work with run-local continuity.",
|
|
637
|
+
skillFocus: "Use for general delegated work when the parent agent wants to choose the task, mode, turn budget, and context shape explicitly.",
|
|
638
|
+
defaultMode: "general",
|
|
639
|
+
defaultMaxTurns: 10
|
|
640
|
+
});
|
|
641
|
+
var delegateExplore = defineDelegateWorkflow({
|
|
642
|
+
id: "delegateExplore",
|
|
643
|
+
description: "Runs read-oriented delegated exploration while preserving run-local continuity.",
|
|
644
|
+
skillName: "trst-delegate-explore",
|
|
645
|
+
skillDescription: "Use as a sub-agent for bounded read-oriented investigation and findings summaries.",
|
|
646
|
+
skillFocus: "Explore mode is read-oriented: inspect code, commands, docs, and artifacts; summarize findings and avoid edits unless the task explicitly asks for them.",
|
|
647
|
+
defaultMode: "explore",
|
|
648
|
+
defaultMaxTurns: 6,
|
|
649
|
+
summarize: true
|
|
650
|
+
});
|
|
651
|
+
var delegateSimpleExplore = defineDelegateWorkflow({
|
|
652
|
+
id: "delegateSimpleExplore",
|
|
653
|
+
description: "Runs narrow, low-cost read-only exploration while preserving run-local continuity.",
|
|
654
|
+
skillName: "trst-delegate-simple-explore",
|
|
655
|
+
skillDescription: "Use as a sub-agent for quick, bounded read-only lookups and summaries.",
|
|
656
|
+
skillFocus: "Simple explore is read-only and narrow: answer focused repo questions, inspect one area, summarize concise findings, and avoid edits.",
|
|
657
|
+
defaultMode: "explore",
|
|
658
|
+
defaultMaxTurns: 4,
|
|
659
|
+
summarize: true,
|
|
660
|
+
agentSize: "small",
|
|
661
|
+
agentThinking: "low"
|
|
662
|
+
});
|
|
663
|
+
var delegateArchitectPlanner = defineDelegateWorkflow({
|
|
664
|
+
id: "delegateArchitectPlanner",
|
|
665
|
+
description: "Scouts the codebase to identify the few files or seams likely needing edits.",
|
|
666
|
+
skillName: "trst-delegate-architect-planner",
|
|
667
|
+
skillDescription: "Use as a read-only scout that maps a bug or feature request to likely edit targets.",
|
|
668
|
+
skillFocus: "Architect planner is read-only: grep/read the repo, identify the 3-4 files or seams most likely to need edits, explain why, and do not write code.",
|
|
669
|
+
defaultMode: "explore",
|
|
670
|
+
defaultMaxTurns: 6,
|
|
671
|
+
summarize: true,
|
|
672
|
+
agentSize: "medium",
|
|
673
|
+
agentThinking: "medium"
|
|
674
|
+
});
|
|
675
|
+
var delegateReview = defineDelegateWorkflow({
|
|
676
|
+
id: "delegateReview",
|
|
677
|
+
description: "Runs delegated review work while preserving run-local continuity.",
|
|
678
|
+
skillName: "trst-delegate-review",
|
|
679
|
+
skillDescription: "Use as a sub-agent for focused code, plan, or change review without unnecessary edits.",
|
|
680
|
+
skillFocus: "Review mode should inspect existing work, identify concrete issues and risks, and avoid edits unless the parent task explicitly requests a fix.",
|
|
681
|
+
defaultMode: "review",
|
|
682
|
+
defaultMaxTurns: 6,
|
|
683
|
+
summarize: false
|
|
684
|
+
});
|
|
685
|
+
var delegateImplement = defineDelegateWorkflow({
|
|
686
|
+
id: "delegateImplement",
|
|
687
|
+
description: "Runs delegated implementation work while preserving run-local continuity.",
|
|
688
|
+
skillName: "trst-delegate-implement",
|
|
689
|
+
skillDescription: "Use as a sub-agent for bounded implementation tasks that may edit files.",
|
|
690
|
+
skillFocus: "Implement mode may edit files. Make the smallest safe change, run focused checks when practical, and always report changedFiles/artifacts that were actually produced.",
|
|
691
|
+
defaultMode: "implement",
|
|
692
|
+
defaultMaxTurns: 12,
|
|
693
|
+
summarize: false,
|
|
694
|
+
agentThinking: "medium"
|
|
695
|
+
});
|
|
696
|
+
var delegateQuickImplementor = defineDelegateWorkflow({
|
|
697
|
+
id: "delegateQuickImplementor",
|
|
698
|
+
description: "Runs strict localized implementation work for mechanical changes.",
|
|
699
|
+
skillName: "trst-delegate-quick-implementor",
|
|
700
|
+
skillDescription: "Use as a sub-agent for strict, localized boilerplate implementation tasks.",
|
|
701
|
+
skillFocus: "Quick implementor is the typist: follow concrete instructions, make localized edits only, avoid broad architecture decisions, run focused checks when practical, and report changedFiles.",
|
|
702
|
+
defaultMode: "implement",
|
|
703
|
+
defaultMaxTurns: 8,
|
|
704
|
+
summarize: false,
|
|
705
|
+
agentSize: "medium",
|
|
706
|
+
agentThinking: "medium"
|
|
707
|
+
});
|
|
708
|
+
var delegateSmartImplementor = defineDelegateWorkflow({
|
|
709
|
+
id: "delegateSmartImplementor",
|
|
710
|
+
description: "Runs complex implementation, algorithmic work, or cross-file refactors.",
|
|
711
|
+
skillName: "trst-delegate-smart-implementor",
|
|
712
|
+
skillDescription: "Use as a sub-agent for complex logic, cross-file refactoring, or algorithmic design.",
|
|
713
|
+
skillFocus: "Smart implementor is the heavy-lifter: handle complex interconnected changes, refactors, algorithms, and non-trivial systems; keep scope bounded and report changedFiles.",
|
|
714
|
+
defaultMode: "implement",
|
|
715
|
+
defaultMaxTurns: 14,
|
|
716
|
+
summarize: false,
|
|
717
|
+
agentSize: "large",
|
|
718
|
+
agentThinking: "high"
|
|
719
|
+
});
|
|
720
|
+
var delegateRelentlessDebugger = defineDelegateWorkflow({
|
|
721
|
+
id: "delegateRelentlessDebugger",
|
|
722
|
+
description: "Diagnoses and fixes validation, lint, test, or runtime failures.",
|
|
723
|
+
skillName: "trst-delegate-relentless-debugger",
|
|
724
|
+
skillDescription: "Use as a sub-agent for repairing linter failures, test failures, stack traces, and diagnostics.",
|
|
725
|
+
skillFocus: "Relentless debugger is the janitor: consume diagnostics, preserve the intended change, fix failures iteratively, and avoid redesign unless required by the failure evidence.",
|
|
726
|
+
defaultMode: "implement",
|
|
727
|
+
defaultMaxTurns: 12,
|
|
728
|
+
summarize: false,
|
|
729
|
+
agentSize: "large",
|
|
730
|
+
agentThinking: "high"
|
|
731
|
+
});
|
|
732
|
+
var delegateSchemaFormatter = defineDelegateWorkflow({
|
|
733
|
+
id: "delegateSchemaFormatter",
|
|
734
|
+
description: "Produces exact structured output for schema-bound handoffs.",
|
|
735
|
+
skillName: "trst-delegate-schema-formatter",
|
|
736
|
+
skillDescription: "Use as a sub-agent for exact JSON/XML/schema-constrained formatting tasks.",
|
|
737
|
+
skillFocus: "Schema formatter is the translator: prioritize exact requested structure, avoid extra prose, and do not perform broad implementation work unless explicitly requested.",
|
|
738
|
+
defaultMode: "general",
|
|
739
|
+
defaultMaxTurns: 4,
|
|
740
|
+
summarize: false,
|
|
741
|
+
agentSize: "small",
|
|
742
|
+
agentThinking: "low"
|
|
743
|
+
});
|
|
744
|
+
function delegateSkill(config) {
|
|
745
|
+
return `---
|
|
746
|
+
name: ${config.skillName}
|
|
747
|
+
description: ${config.skillDescription}
|
|
748
|
+
x-trailstep-user-facing: false
|
|
749
|
+
---
|
|
750
|
+
# TrailStep ${config.id} workflow
|
|
751
|
+
|
|
752
|
+
${config.skillFocus}
|
|
753
|
+
|
|
754
|
+
Use this workflow as a parent-agent/delegate tool when a parent agent needs bounded work in a repository. The delegate keeps continuity only inside one TrailStep run; separate runs do not share memory.
|
|
755
|
+
|
|
756
|
+
## Parent-agent usage
|
|
757
|
+
|
|
758
|
+
- For simple tasks, pass direct flags: \`trailstep @trailstep/sub-agents#${config.id} --task "..."\`.
|
|
759
|
+
- For one-shot JSON, pipe it to \`trailstep @trailstep/sub-agents#${config.id} --input-file -\`; for long/reusable context, write an input JSON file and run \`trailstep @trailstep/sub-agents#${config.id} --input-file delegate-input.json\`.
|
|
760
|
+
- Use unique run names for parallel delegates so their run-local state stays separate.
|
|
761
|
+
- Use \`cwd\` for an existing worktree or subdirectory that TrailStep should not manage. Use \`worktree.enabled=true\` when the delegate should create a managed git worktree and safely clean it up/report it.
|
|
762
|
+
- Do not pass both \`cwd\` and \`worktree.enabled=true\`; choose one execution location strategy.
|
|
763
|
+
- If a delegate asks a question, answer with \`trailstep answer <runName> parent-answer --json '{"answer":"..."}'\`, then resume with \`trailstep continue <runName>\`.
|
|
764
|
+
- Retry a failed turn with \`trailstep retry <workflowRef> <runName>\` when appropriate.
|
|
765
|
+
- Read final typed output with \`trailstep output <runName>\`; read the terminal summary with \`trailstep output <runName> --message\`.
|
|
766
|
+
- Package reference: \`@trailstep/sub-agents#${config.id}\`; built dist reference: \`./packages/sub-agents/dist/index.js#${config.id}\`.
|
|
767
|
+
|
|
768
|
+
This skill is intended for delegated working-agent use rather than direct end-user planning chat. Preserve the run-local memory supplied in the workflow prompt, keep work bounded to the requested turn, and return the JSON contract requested by the workflow prompt.`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/delegate-parallel/workflow.ts
|
|
772
|
+
import { defineWorkflow as defineWorkflow2, done as done2, parallel, state as state2, step as step2 } from "@trailstep/authoring";
|
|
773
|
+
|
|
774
|
+
// src/delegate-parallel/schema.ts
|
|
775
|
+
import { jsonSchema as jsonSchema2 } from "@trailstep/authoring";
|
|
776
|
+
var delegateParallelInputShape = jsonSchema2({
|
|
777
|
+
type: "object",
|
|
778
|
+
properties: {
|
|
779
|
+
context: { type: "string" },
|
|
780
|
+
cwd: { type: "string" },
|
|
781
|
+
maxTurns: { type: "number" },
|
|
782
|
+
summarize: { type: "boolean" },
|
|
783
|
+
worktree: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: {
|
|
786
|
+
enabled: { type: "boolean" },
|
|
787
|
+
path: { type: "string" },
|
|
788
|
+
baseRef: { type: "string" },
|
|
789
|
+
baseBranch: { type: "string" },
|
|
790
|
+
branch: { type: "string" },
|
|
791
|
+
cleanup: { type: "string", enum: ["auto", "never", "always"] },
|
|
792
|
+
forceCleanup: { type: "boolean" }
|
|
793
|
+
},
|
|
794
|
+
additionalProperties: false
|
|
795
|
+
},
|
|
796
|
+
tasks: {
|
|
797
|
+
type: "array",
|
|
798
|
+
items: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
id: { type: "string" },
|
|
802
|
+
mode: { type: "string", enum: ["explore", "implement", "review", "general"] },
|
|
803
|
+
delegate: {
|
|
804
|
+
type: "string",
|
|
805
|
+
enum: [
|
|
806
|
+
"delegate",
|
|
807
|
+
"simple-explore",
|
|
808
|
+
"architect-planner",
|
|
809
|
+
"quick-implementor",
|
|
810
|
+
"smart-implementor",
|
|
811
|
+
"relentless-debugger",
|
|
812
|
+
"fixer",
|
|
813
|
+
"schema-formatter"
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
|
+
task: { type: "string" },
|
|
817
|
+
context: { type: "string" },
|
|
818
|
+
cwd: { type: "string" },
|
|
819
|
+
maxTurns: { type: "number" },
|
|
820
|
+
summarize: { type: "boolean" },
|
|
821
|
+
worktree: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: {
|
|
824
|
+
enabled: { type: "boolean" },
|
|
825
|
+
path: { type: "string" },
|
|
826
|
+
baseRef: { type: "string" },
|
|
827
|
+
baseBranch: { type: "string" },
|
|
828
|
+
branch: { type: "string" },
|
|
829
|
+
cleanup: { type: "string", enum: ["auto", "never", "always"] },
|
|
830
|
+
forceCleanup: { type: "boolean" }
|
|
831
|
+
},
|
|
832
|
+
additionalProperties: false
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
anyOf: [{ required: ["mode"] }, { required: ["delegate"] }],
|
|
836
|
+
required: ["id", "task"],
|
|
837
|
+
additionalProperties: false
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
required: ["tasks"],
|
|
842
|
+
additionalProperties: false
|
|
843
|
+
});
|
|
844
|
+
var delegateParallelOutputShape = jsonSchema2({
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
status: { type: "string", enum: ["completed", "blocked"] },
|
|
848
|
+
summary: { type: "string" },
|
|
849
|
+
branches: { type: "object", additionalProperties: true }
|
|
850
|
+
},
|
|
851
|
+
required: ["status"],
|
|
852
|
+
additionalProperties: false
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// src/delegate-parallel/workflow.ts
|
|
856
|
+
var delegateParallel = defineWorkflow2({
|
|
857
|
+
id: "delegateParallel",
|
|
858
|
+
description: "Fans out required delegate tasks across parallel delegate workflows.",
|
|
859
|
+
skill: delegateParallelSkill(),
|
|
860
|
+
inputShape: delegateParallelInputShape,
|
|
861
|
+
outputShape: delegateParallelOutputShape,
|
|
862
|
+
start(input) {
|
|
863
|
+
return initializeDelegateParallelStep(input);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
var initializeDelegateParallelStep = step2({
|
|
867
|
+
id: "initialize-delegate-parallel",
|
|
868
|
+
title: "Initialize parallel delegates"
|
|
869
|
+
}).display(({ input }) => `Preparing ${input.tasks.length} delegates`).do((input) => {
|
|
870
|
+
const normalized = normalizeDelegateParallelTasks(input, state2.name);
|
|
871
|
+
if (normalized.status === "blocked") {
|
|
872
|
+
return done2({ status: "blocked", summary: normalized.summary });
|
|
873
|
+
}
|
|
874
|
+
return parallel(
|
|
875
|
+
normalized.tasks.map(
|
|
876
|
+
(task) => delegateWorkflowForTask(task)(task.input, { branchId: task.branchId })
|
|
877
|
+
)
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
function normalizeDelegateParallelTasks(input, runName) {
|
|
881
|
+
if (input.tasks.length === 0) {
|
|
882
|
+
return { status: "blocked", summary: "delegateParallel requires at least one task." };
|
|
883
|
+
}
|
|
884
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
885
|
+
const seenBranches = /* @__PURE__ */ new Set();
|
|
886
|
+
const tasks = [];
|
|
887
|
+
for (const task of input.tasks) {
|
|
888
|
+
const id = task.id.trim();
|
|
889
|
+
const taskText = task.task.trim();
|
|
890
|
+
if (id.length === 0) {
|
|
891
|
+
return { status: "blocked", summary: "delegateParallel task ids must be non-empty." };
|
|
892
|
+
}
|
|
893
|
+
if (taskText.length === 0) {
|
|
894
|
+
return {
|
|
895
|
+
status: "blocked",
|
|
896
|
+
summary: `delegateParallel task ${id} must include non-empty task text.`
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
if (seenIds.has(id)) {
|
|
900
|
+
return { status: "blocked", summary: `delegateParallel task id '${id}' is duplicated.` };
|
|
901
|
+
}
|
|
902
|
+
seenIds.add(id);
|
|
903
|
+
const branchId = `delegate-${stableSegment(id)}`;
|
|
904
|
+
if (seenBranches.has(branchId)) {
|
|
905
|
+
return {
|
|
906
|
+
status: "blocked",
|
|
907
|
+
summary: `delegateParallel task id '${id}' normalizes to duplicate branch id '${branchId}'.`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
seenBranches.add(branchId);
|
|
911
|
+
const delegateSelection = normalizeDelegateSelection(task.delegate);
|
|
912
|
+
const mode = task.mode ?? defaultModeForDelegate(delegateSelection);
|
|
913
|
+
const cwd = nonEmpty(task.cwd) ?? nonEmpty(input.cwd);
|
|
914
|
+
const worktree = normalizeTaskWorktree({
|
|
915
|
+
parent: input.worktree,
|
|
916
|
+
task: task.worktree,
|
|
917
|
+
cwd,
|
|
918
|
+
runName,
|
|
919
|
+
taskSegment: stableSegment(id)
|
|
920
|
+
});
|
|
921
|
+
const context = combineContext(input.context, task.context);
|
|
922
|
+
const maxTurns = task.maxTurns ?? input.maxTurns;
|
|
923
|
+
const summarize = task.summarize ?? input.summarize;
|
|
924
|
+
tasks.push({
|
|
925
|
+
id,
|
|
926
|
+
mode,
|
|
927
|
+
...delegateSelection === void 0 ? {} : { delegate: delegateSelection },
|
|
928
|
+
branchId,
|
|
929
|
+
input: {
|
|
930
|
+
task: taskText,
|
|
931
|
+
mode,
|
|
932
|
+
...context === void 0 ? {} : { context },
|
|
933
|
+
...cwd === void 0 ? {} : { cwd },
|
|
934
|
+
...maxTurns === void 0 ? {} : { maxTurns },
|
|
935
|
+
...summarize === void 0 ? {} : { summarize },
|
|
936
|
+
...worktree === void 0 ? {} : { worktree }
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return { status: "ready", tasks };
|
|
941
|
+
}
|
|
942
|
+
function delegateWorkflowForTask(task) {
|
|
943
|
+
switch (task.delegate) {
|
|
944
|
+
case "simple-explore":
|
|
945
|
+
return delegateSimpleExplore;
|
|
946
|
+
case "architect-planner":
|
|
947
|
+
return delegateArchitectPlanner;
|
|
948
|
+
case "quick-implementor":
|
|
949
|
+
return delegateQuickImplementor;
|
|
950
|
+
case "smart-implementor":
|
|
951
|
+
return delegateSmartImplementor;
|
|
952
|
+
case "relentless-debugger":
|
|
953
|
+
case "fixer":
|
|
954
|
+
return delegateRelentlessDebugger;
|
|
955
|
+
case "schema-formatter":
|
|
956
|
+
return delegateSchemaFormatter;
|
|
957
|
+
case "delegate":
|
|
958
|
+
case void 0:
|
|
959
|
+
return delegateWorkflowForMode(task.mode);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function delegateWorkflowForMode(mode) {
|
|
963
|
+
switch (mode) {
|
|
964
|
+
case "explore":
|
|
965
|
+
return delegateExplore;
|
|
966
|
+
case "implement":
|
|
967
|
+
return delegateImplement;
|
|
968
|
+
case "review":
|
|
969
|
+
return delegateReview;
|
|
970
|
+
case "general":
|
|
971
|
+
return delegate;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function normalizeDelegateSelection(value) {
|
|
975
|
+
return value === void 0 ? void 0 : value;
|
|
976
|
+
}
|
|
977
|
+
function defaultModeForDelegate(delegateSelection) {
|
|
978
|
+
switch (delegateSelection) {
|
|
979
|
+
case "simple-explore":
|
|
980
|
+
case "architect-planner":
|
|
981
|
+
return "explore";
|
|
982
|
+
case "quick-implementor":
|
|
983
|
+
case "smart-implementor":
|
|
984
|
+
case "relentless-debugger":
|
|
985
|
+
case "fixer":
|
|
986
|
+
return "implement";
|
|
987
|
+
case "schema-formatter":
|
|
988
|
+
case "delegate":
|
|
989
|
+
case void 0:
|
|
990
|
+
return "general";
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function normalizeTaskWorktree(input) {
|
|
994
|
+
if (input.cwd !== void 0) {
|
|
995
|
+
return input.task?.enabled === true ? input.task : void 0;
|
|
996
|
+
}
|
|
997
|
+
const enabled = input.task?.enabled ?? input.parent?.enabled;
|
|
998
|
+
if (enabled !== true) {
|
|
999
|
+
return input.task ?? input.parent;
|
|
1000
|
+
}
|
|
1001
|
+
const merged = { ...input.parent, ...input.task, enabled: true };
|
|
1002
|
+
return {
|
|
1003
|
+
...merged,
|
|
1004
|
+
path: nonEmpty(merged.path) ?? `.trailstep/worktrees/${input.runName}/${input.taskSegment}`,
|
|
1005
|
+
branch: nonEmpty(merged.branch) ?? `trailstep/delegate/${input.runName}/${input.taskSegment}`
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function combineContext(parent, task) {
|
|
1009
|
+
const parentContext = nonEmpty(parent);
|
|
1010
|
+
const taskContext = nonEmpty(task);
|
|
1011
|
+
if (parentContext === void 0) {
|
|
1012
|
+
return taskContext;
|
|
1013
|
+
}
|
|
1014
|
+
if (taskContext === void 0) {
|
|
1015
|
+
return parentContext;
|
|
1016
|
+
}
|
|
1017
|
+
return `${parentContext}
|
|
1018
|
+
|
|
1019
|
+
${taskContext}`;
|
|
1020
|
+
}
|
|
1021
|
+
function nonEmpty(value) {
|
|
1022
|
+
const trimmed = value?.trim();
|
|
1023
|
+
return trimmed && trimmed.length > 0 ? trimmed : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function stableSegment(value) {
|
|
1026
|
+
const segment = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1027
|
+
return segment.length > 0 ? segment : `task-${hashString(value)}`;
|
|
1028
|
+
}
|
|
1029
|
+
function hashString(value) {
|
|
1030
|
+
let hash = 2166136261;
|
|
1031
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1032
|
+
hash ^= value.charCodeAt(index);
|
|
1033
|
+
hash = Math.imul(hash, 16777619);
|
|
1034
|
+
}
|
|
1035
|
+
return (hash >>> 0).toString(16);
|
|
1036
|
+
}
|
|
1037
|
+
function delegateParallelSkill() {
|
|
1038
|
+
return `---
|
|
1039
|
+
name: trst-delegate-parallel
|
|
1040
|
+
description: Use as a parent-agent fan-out tool for required TrailStep delegate tasks that can run in parallel.
|
|
1041
|
+
x-trailstep-user-facing: false
|
|
1042
|
+
---
|
|
1043
|
+
# TrailStep delegateParallel workflow
|
|
1044
|
+
|
|
1045
|
+
Use this workflow when a parent agent already has a set of concrete, independent delegate tasks. It fans out directly to the existing delegate, delegateExplore, delegateReview, and delegateImplement workflows based on each task mode. It does not add a planning agent and does not merge results beyond returning the parallel branch outputs.
|
|
1046
|
+
|
|
1047
|
+
Each task must provide a stable \`id\`, \`mode\`, and \`task\` text. Shared \`context\`, \`cwd\`, \`maxTurns\`, \`summarize\`, and \`worktree\` values are applied as defaults; task-level values override them. Managed worktrees under \`worktree.enabled=true\` default per task to \`.trailstep/worktrees/<runName>/<taskId>\` and \`trailstep/delegate/<runName>/<taskId>\` to avoid collisions.`;
|
|
1048
|
+
}
|
|
1049
|
+
export {
|
|
1050
|
+
delegate,
|
|
1051
|
+
delegateArchitectPlanner,
|
|
1052
|
+
delegateExplore,
|
|
1053
|
+
delegateImplement,
|
|
1054
|
+
delegateParallel,
|
|
1055
|
+
delegateQuickImplementor,
|
|
1056
|
+
delegateRelentlessDebugger,
|
|
1057
|
+
delegateReview,
|
|
1058
|
+
delegateSchemaFormatter,
|
|
1059
|
+
delegateSimpleExplore,
|
|
1060
|
+
delegateSmartImplementor
|
|
1061
|
+
};
|
|
1062
|
+
//# sourceMappingURL=index.js.map
|