stageflow 0.39.0 → 0.40.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/README.md +43 -18
- package/dist/agent/claudeAdapter.js +15 -1
- package/dist/agent/claudeTools.d.ts +13 -0
- package/dist/agent/claudeTools.js +98 -0
- package/dist/agent/piAdapter.js +38 -16
- package/dist/agent/sandboxPiTools.d.ts +99 -0
- package/dist/agent/sandboxPiTools.js +109 -0
- package/dist/config/hostConfig.js +2 -0
- package/dist/config/loadTask.js +34 -1
- package/dist/mcp/catalogTools.js +11 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
- package/dist/runtime/gitCheckoutCapability.js +1 -1
- package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
- package/dist/runtime/gitWorktreeCheckout.js +120 -0
- package/dist/runtime/sandboxContainer.d.ts +62 -0
- package/dist/runtime/sandboxContainer.js +151 -0
- package/dist/runtime/sandboxDesk.d.ts +28 -0
- package/dist/runtime/sandboxDesk.js +242 -0
- package/dist/runtime/sandboxFiles.d.ts +20 -0
- package/dist/runtime/sandboxFiles.js +150 -0
- package/dist/runtime/stageEnvironment.js +2 -0
- package/dist/runtime/stageRoots.d.ts +9 -0
- package/dist/runtime/stageRoots.js +14 -0
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +38 -4
- package/dist/runtime/stageSandbox.d.ts +16 -0
- package/dist/runtime/stageSandbox.js +54 -0
- package/dist/runtime/stageWorker.js +138 -105
- package/dist/runtime/workspaceBinding.d.ts +2 -1
- package/dist/runtime/workspaceBinding.js +20 -4
- package/dist/tools/readEmail.d.ts +3 -3
- package/dist/types/task.d.ts +11 -1
- package/examples/memory/README.md +2 -0
- package/examples/memory/probe-bash.task.yaml +3 -0
- package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
- package/examples/ship-feature/README.md +15 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { bindPiAgentDirEnv, overlayStageBindingEnv, rootsForStageWorker, stageBi
|
|
|
11
11
|
import { buildStageEnvironment } from "./stageEnvironment.js";
|
|
12
12
|
import { ensureStageCacheDirs } from "./stageCacheEnv.js";
|
|
13
13
|
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
14
|
+
import { openStageSandbox } from "./stageSandbox.js";
|
|
14
15
|
import { runStage, isRunStageWaiting, } from "./stageRunner.js";
|
|
15
16
|
import { openStageAttempt } from "./stageAttemptBootstrap.js";
|
|
16
17
|
import { attemptContext } from "./stageAttemptContext.js";
|
|
@@ -60,97 +61,137 @@ export async function runStageWorker(input) {
|
|
|
60
61
|
.filter((e) => e[1] !== undefined));
|
|
61
62
|
const mode = input.mode ?? "run";
|
|
62
63
|
const attempt = input.attempt ?? 1;
|
|
63
|
-
|
|
64
|
+
const attemptDir = attemptWorkspaceDir(workspaceDir, input.stageId, attempt);
|
|
65
|
+
loadNamedSecretsFromAttemptDir(attemptDir);
|
|
64
66
|
const attemptCtx = input.attempt !== undefined ? attemptContext(input.attempt) : undefined;
|
|
65
67
|
const eventOptions = { attempt };
|
|
66
|
-
|
|
68
|
+
let roots = withResolvedAuthPath(rootsForStageWorker(workspaceDir, input.stageId, stage.model, checkoutRoot, attemptCtx), input.rootDir);
|
|
67
69
|
const unbindAgentDir = bindPiAgentDirEnv(roots.agentDir);
|
|
70
|
+
const sandbox = await openStageSandbox({
|
|
71
|
+
env: process.env,
|
|
72
|
+
roots,
|
|
73
|
+
runId: input.runId,
|
|
74
|
+
stageId: input.stageId,
|
|
75
|
+
attempt,
|
|
76
|
+
attemptDir,
|
|
77
|
+
...(checkoutRoot !== undefined ? { checkoutRoot } : {}),
|
|
78
|
+
});
|
|
79
|
+
roots = sandbox.roots;
|
|
80
|
+
const beforeVerify = sandbox.beforeVerify;
|
|
81
|
+
let outcome;
|
|
82
|
+
let thrown;
|
|
68
83
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
store,
|
|
73
|
-
runId: input.runId,
|
|
74
|
-
stage,
|
|
75
|
-
stageId: input.stageId,
|
|
76
|
-
task,
|
|
77
|
-
dag,
|
|
78
|
-
checkoutRoot,
|
|
79
|
-
workspaceDir,
|
|
80
|
-
factoryCwd: input.rootDir,
|
|
81
|
-
attemptCtx,
|
|
82
|
-
operatorCatalog: input.operatorCatalog,
|
|
83
|
-
roots,
|
|
84
|
-
resumeToken: input.sessionFilePath,
|
|
85
|
-
sessionMode: "waiting_resume",
|
|
86
|
-
stageEnv,
|
|
87
|
-
onActivity: (event) => {
|
|
88
|
-
void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
if (!opened.ok) {
|
|
92
|
-
return { ok: false, reason: opened.reason };
|
|
84
|
+
outcome = await (async () => {
|
|
85
|
+
if (sandbox.failure !== undefined) {
|
|
86
|
+
return { ok: false, reason: sandbox.failure };
|
|
93
87
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
if (mode === "resume") {
|
|
89
|
+
const opened = await openStageAttempt({
|
|
90
|
+
agent,
|
|
91
|
+
store,
|
|
92
|
+
runId: input.runId,
|
|
93
|
+
stage,
|
|
94
|
+
stageId: input.stageId,
|
|
95
|
+
task,
|
|
96
|
+
dag,
|
|
97
|
+
checkoutRoot,
|
|
98
|
+
workspaceDir,
|
|
99
|
+
factoryCwd: input.rootDir,
|
|
100
|
+
attemptCtx,
|
|
101
|
+
operatorCatalog: input.operatorCatalog,
|
|
102
|
+
roots,
|
|
103
|
+
resumeToken: input.sessionFilePath,
|
|
104
|
+
sessionMode: "waiting_resume",
|
|
105
|
+
stageEnv,
|
|
106
|
+
onActivity: (event) => {
|
|
107
|
+
void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (!opened.ok) {
|
|
111
|
+
return { ok: false, reason: opened.reason };
|
|
112
|
+
}
|
|
113
|
+
opened.handle.deliverAnswer(input.resumeAnswer);
|
|
114
|
+
const outcome = await runStage({
|
|
115
|
+
agent,
|
|
116
|
+
store,
|
|
117
|
+
runId: input.runId,
|
|
118
|
+
stage,
|
|
119
|
+
stageId: input.stageId,
|
|
120
|
+
task,
|
|
121
|
+
dag,
|
|
122
|
+
checkoutRoot,
|
|
123
|
+
workspaceDir,
|
|
124
|
+
skipStarted: true,
|
|
125
|
+
existingHandle: opened.handle,
|
|
126
|
+
workerMode: true,
|
|
127
|
+
roots,
|
|
128
|
+
attemptCtx,
|
|
129
|
+
factoryCwd: input.rootDir,
|
|
130
|
+
operatorCatalog: input.operatorCatalog,
|
|
131
|
+
skipGates: input.skipGates,
|
|
132
|
+
stageEnv,
|
|
133
|
+
beforeVerify,
|
|
134
|
+
});
|
|
135
|
+
return outcome;
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
(mode === "feedback_resume" &&
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
if (mode === "feedback_resume" || mode === "new_session") {
|
|
138
|
+
const feedbackLoopContext = await loadActiveFeedbackLoopContext(store, input.runId, input.stageId);
|
|
139
|
+
if (mode === "feedback_resume" && feedbackLoopContext === undefined) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: "feedback_resume requires an active feedback-loop context",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const priorAttempt = feedbackLoopContext?.prior_stage_attempt;
|
|
146
|
+
const resumeToken = input.sessionFilePath ??
|
|
147
|
+
(mode === "feedback_resume" && priorAttempt !== undefined
|
|
148
|
+
? attemptContext(priorAttempt).sessionPath(workspaceDir, input.stageId)
|
|
149
|
+
: undefined);
|
|
150
|
+
const opened = await openStageAttempt({
|
|
151
|
+
agent,
|
|
152
|
+
store,
|
|
153
|
+
runId: input.runId,
|
|
154
|
+
stage,
|
|
155
|
+
stageId: input.stageId,
|
|
156
|
+
task,
|
|
157
|
+
dag,
|
|
158
|
+
checkoutRoot,
|
|
159
|
+
workspaceDir,
|
|
160
|
+
factoryCwd: input.rootDir,
|
|
161
|
+
attemptCtx,
|
|
162
|
+
operatorCatalog: input.operatorCatalog,
|
|
163
|
+
roots,
|
|
164
|
+
...(resumeToken !== undefined ? { resumeToken } : {}),
|
|
165
|
+
sessionMode: mode,
|
|
166
|
+
...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
|
|
167
|
+
stageEnv,
|
|
168
|
+
onActivity: (event) => {
|
|
169
|
+
void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (!opened.ok) {
|
|
173
|
+
return { ok: false, reason: opened.reason };
|
|
174
|
+
}
|
|
175
|
+
return runStage({
|
|
176
|
+
agent,
|
|
177
|
+
store,
|
|
178
|
+
runId: input.runId,
|
|
179
|
+
stage,
|
|
180
|
+
stageId: input.stageId,
|
|
181
|
+
task,
|
|
182
|
+
dag,
|
|
183
|
+
checkoutRoot,
|
|
184
|
+
workspaceDir,
|
|
185
|
+
existingHandle: opened.handle,
|
|
186
|
+
workerMode: true,
|
|
187
|
+
roots,
|
|
188
|
+
attemptCtx,
|
|
189
|
+
factoryCwd: input.rootDir,
|
|
190
|
+
operatorCatalog: input.operatorCatalog,
|
|
191
|
+
skipGates: input.skipGates,
|
|
192
|
+
stageEnv,
|
|
193
|
+
beforeVerify,
|
|
194
|
+
});
|
|
154
195
|
}
|
|
155
196
|
return runStage({
|
|
156
197
|
agent,
|
|
@@ -162,7 +203,6 @@ export async function runStageWorker(input) {
|
|
|
162
203
|
dag,
|
|
163
204
|
checkoutRoot,
|
|
164
205
|
workspaceDir,
|
|
165
|
-
existingHandle: opened.handle,
|
|
166
206
|
workerMode: true,
|
|
167
207
|
roots,
|
|
168
208
|
attemptCtx,
|
|
@@ -170,30 +210,23 @@ export async function runStageWorker(input) {
|
|
|
170
210
|
operatorCatalog: input.operatorCatalog,
|
|
171
211
|
skipGates: input.skipGates,
|
|
172
212
|
stageEnv,
|
|
213
|
+
beforeVerify,
|
|
173
214
|
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
runId: input.runId,
|
|
179
|
-
stage,
|
|
180
|
-
stageId: input.stageId,
|
|
181
|
-
task,
|
|
182
|
-
dag,
|
|
183
|
-
checkoutRoot,
|
|
184
|
-
workspaceDir,
|
|
185
|
-
workerMode: true,
|
|
186
|
-
roots,
|
|
187
|
-
attemptCtx,
|
|
188
|
-
factoryCwd: input.rootDir,
|
|
189
|
-
operatorCatalog: input.operatorCatalog,
|
|
190
|
-
skipGates: input.skipGates,
|
|
191
|
-
stageEnv,
|
|
192
|
-
});
|
|
215
|
+
})();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
thrown = err;
|
|
193
219
|
}
|
|
194
220
|
finally {
|
|
195
221
|
unbindAgentDir();
|
|
222
|
+
await sandbox.close();
|
|
223
|
+
}
|
|
224
|
+
if (thrown !== undefined)
|
|
225
|
+
throw thrown;
|
|
226
|
+
if (outcome === undefined) {
|
|
227
|
+
throw new Error("stage worker finished without an outcome");
|
|
196
228
|
}
|
|
229
|
+
return outcome;
|
|
197
230
|
}
|
|
198
231
|
export function sendWorkerResult(result) {
|
|
199
232
|
if (typeof process.send !== "function") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LoadOutcome } from "../config/loadOutcome.js";
|
|
2
|
+
import type { CheckoutDescriptor } from "../types/task.js";
|
|
2
3
|
export type WorkspaceBinding = {
|
|
3
4
|
kind: "repository";
|
|
4
5
|
repository: string;
|
|
@@ -12,7 +13,7 @@ export type WorkspaceBinding = {
|
|
|
12
13
|
export type WorkspaceBindingInput = {
|
|
13
14
|
repository?: string;
|
|
14
15
|
ref?: string;
|
|
15
|
-
checkout?:
|
|
16
|
+
checkout?: CheckoutDescriptor;
|
|
16
17
|
};
|
|
17
18
|
export type ResolveWorkspaceBindingOptions = {
|
|
18
19
|
checkoutOverride?: string;
|
|
@@ -9,12 +9,25 @@ function nonEmptyTrimmed(value) {
|
|
|
9
9
|
function isGithubOwnerRepo(repository) {
|
|
10
10
|
return OWNER_REPO_RE.test(repository);
|
|
11
11
|
}
|
|
12
|
+
function resolveCheckoutBinding(checkout, checkoutOverride) {
|
|
13
|
+
const override = nonEmptyTrimmed(checkoutOverride);
|
|
14
|
+
if (override !== undefined)
|
|
15
|
+
return override;
|
|
16
|
+
if (checkout === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof checkout === "string")
|
|
19
|
+
return nonEmptyTrimmed(checkout);
|
|
20
|
+
if (typeof checkout === "object" && checkout !== null && !Array.isArray(checkout)) {
|
|
21
|
+
return checkout;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
12
25
|
export function resolveWorkspaceBinding(task, options) {
|
|
13
26
|
const repository = nonEmptyTrimmed(task.repository);
|
|
14
27
|
const ref = nonEmptyTrimmed(task.ref);
|
|
15
|
-
const
|
|
28
|
+
const checkoutBinding = resolveCheckoutBinding(task.checkout, options?.checkoutOverride);
|
|
16
29
|
const hasRepositoryField = repository !== undefined;
|
|
17
|
-
if (hasRepositoryField &&
|
|
30
|
+
if (hasRepositoryField && checkoutBinding !== undefined) {
|
|
18
31
|
return loadFailure([
|
|
19
32
|
{
|
|
20
33
|
code: "task.binding_conflict",
|
|
@@ -53,8 +66,11 @@ export function resolveWorkspaceBinding(task, options) {
|
|
|
53
66
|
}
|
|
54
67
|
return loadSuccess({ kind: "repository", repository, ref });
|
|
55
68
|
}
|
|
56
|
-
if (
|
|
57
|
-
return loadSuccess({ kind: "checkout", path:
|
|
69
|
+
if (typeof checkoutBinding === "string") {
|
|
70
|
+
return loadSuccess({ kind: "checkout", path: checkoutBinding });
|
|
71
|
+
}
|
|
72
|
+
if (typeof checkoutBinding === "object") {
|
|
73
|
+
return loadSuccess({ kind: "checkout", path: "" });
|
|
58
74
|
}
|
|
59
75
|
return loadSuccess({ kind: "unbound" });
|
|
60
76
|
}
|
|
@@ -6,12 +6,12 @@ export declare function createSearchEmailTool(email: StageEmail): {
|
|
|
6
6
|
description: string;
|
|
7
7
|
parameters: Type.TObject<Type.TProperties>;
|
|
8
8
|
execute(_toolCallId: string, input: unknown): Promise<{
|
|
9
|
-
isError?: undefined;
|
|
10
9
|
content: {
|
|
11
10
|
type: "text";
|
|
12
11
|
text: string;
|
|
13
12
|
}[];
|
|
14
13
|
details: unknown;
|
|
14
|
+
isError?: undefined;
|
|
15
15
|
} | {
|
|
16
16
|
content: {
|
|
17
17
|
type: "text";
|
|
@@ -31,12 +31,12 @@ export declare function createGetEmailTool(email: StageEmail): {
|
|
|
31
31
|
description: string;
|
|
32
32
|
parameters: Type.TObject<Type.TProperties>;
|
|
33
33
|
execute(_toolCallId: string, input: unknown): Promise<{
|
|
34
|
-
isError?: undefined;
|
|
35
34
|
content: {
|
|
36
35
|
type: "text";
|
|
37
36
|
text: string;
|
|
38
37
|
}[];
|
|
39
38
|
details: unknown;
|
|
39
|
+
isError?: undefined;
|
|
40
40
|
} | {
|
|
41
41
|
content: {
|
|
42
42
|
type: "text";
|
|
@@ -56,12 +56,12 @@ export declare function createDownloadEmailAttachmentTool(email: StageEmail): {
|
|
|
56
56
|
description: string;
|
|
57
57
|
parameters: Type.TObject<Type.TProperties>;
|
|
58
58
|
execute(_toolCallId: string, input: unknown): Promise<{
|
|
59
|
-
isError?: undefined;
|
|
60
59
|
content: {
|
|
61
60
|
type: "text";
|
|
62
61
|
text: string;
|
|
63
62
|
}[];
|
|
64
63
|
details: unknown;
|
|
64
|
+
isError?: undefined;
|
|
65
65
|
} | {
|
|
66
66
|
content: {
|
|
67
67
|
type: "text";
|
package/dist/types/task.d.ts
CHANGED
|
@@ -2,12 +2,22 @@ export type TaskGitIdentity = {
|
|
|
2
2
|
name?: string;
|
|
3
3
|
email?: string;
|
|
4
4
|
};
|
|
5
|
+
/**
|
|
6
|
+
* A string points at a pre-made checkout directory (validated, never
|
|
7
|
+
* created — today's behavior). An object asks Stageflow to create/reuse a
|
|
8
|
+
* git worktree for `branch` (auto-named from the task id when omitted),
|
|
9
|
+
* branching off `base` (default: HEAD) the first time it's created.
|
|
10
|
+
*/
|
|
11
|
+
export type CheckoutDescriptor = string | {
|
|
12
|
+
branch?: string;
|
|
13
|
+
base?: string;
|
|
14
|
+
};
|
|
5
15
|
export type TaskFile = {
|
|
6
16
|
id: string;
|
|
7
17
|
goal: string;
|
|
8
18
|
context?: string;
|
|
9
19
|
constraints?: string;
|
|
10
|
-
checkout?:
|
|
20
|
+
checkout?: CheckoutDescriptor;
|
|
11
21
|
repository?: string;
|
|
12
22
|
ref?: string;
|
|
13
23
|
run_branch_template?: string;
|
|
@@ -96,4 +96,6 @@ After `desk-revisit-outage`, the release card should be the rollback-finished no
|
|
|
96
96
|
|
|
97
97
|
The operator, in **Memories**, can list keys, open the `TICKET-1842` card, correct it, and delete it. Delete removes the current card and the previous copy and cannot be undone. The page shows the previous card copy and the previous cover text when they exist. The operator can edit the cover. The operator does not create cards; a session adds them. There is one previous copy, not a full history, and no restore action.
|
|
98
98
|
|
|
99
|
+
`sandbox-memory-bash` is the sandbox probe. It attaches `shared-notes` and its `probe` stage runs one bash command, then reports whether that command printed a `body.md` path. The task is `probe-bash`. Create `shared-notes` before running it. Edit the `probe` stage prompt in the pipeline file.
|
|
100
|
+
|
|
99
101
|
This directory is excluded from the repo-root catalog scan, because those commands fail until `notebook` exists.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id: sandbox-memory-bash
|
|
2
|
+
stages:
|
|
3
|
+
- id: probe
|
|
4
|
+
model: openrouter/nvidia/nemotron-3.5-lightning:free
|
|
5
|
+
system_prompt: |
|
|
6
|
+
Use the bash tool to look up the other memory which we dont have access to if its their. it might not be if its not found.
|
|
7
|
+
if found or not then call the following emit stage tool.
|
|
8
|
+
then also add something to the memory you have access to .
|
|
9
|
+
Then call emit_stage_envelope once with status success, a one-line summary, artifacts [], and payload {"found": true if the bash found the other memory, otherwise false, "output":"<the bash stdout>"}.
|
|
10
|
+
io:
|
|
11
|
+
input:
|
|
12
|
+
schema:
|
|
13
|
+
type: object
|
|
14
|
+
additionalProperties: true
|
|
15
|
+
output:
|
|
16
|
+
schema:
|
|
17
|
+
type: object
|
|
18
|
+
required:
|
|
19
|
+
- found
|
|
20
|
+
- output
|
|
21
|
+
properties:
|
|
22
|
+
found:
|
|
23
|
+
type: boolean
|
|
24
|
+
output:
|
|
25
|
+
type: string
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
memories:
|
|
28
|
+
- shared-notes
|
|
@@ -121,6 +121,21 @@ run finishes (or if it fails partway through), remove the worktree with
|
|
|
121
121
|
`git worktree remove ../your-repo-feature-branch` from the main clone once
|
|
122
122
|
you no longer need it.
|
|
123
123
|
|
|
124
|
+
Alternatively, let Stageflow do this for you: point `checkout` at an object
|
|
125
|
+
instead of a path —
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
checkout:
|
|
129
|
+
branch: your-feature-branch # optional — defaults to stageflow/<task id>
|
|
130
|
+
base: origin/main # optional — defaults to HEAD
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
— and Stageflow creates (or reuses, if it already exists) a worktree for
|
|
134
|
+
that branch the same way, as a sibling of your project root
|
|
135
|
+
(`../.stageflow-worktrees/<branch>`). It's never auto-removed, same as one
|
|
136
|
+
you make by hand — clean it up yourself with `git worktree remove` when
|
|
137
|
+
you're done.
|
|
138
|
+
|
|
124
139
|
## Create a task
|
|
125
140
|
|
|
126
141
|
Task context is the pipeline's portable feature brief. Include enough detail
|