Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/taskflow-core/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type CacheScope = (typeof CACHE_SCOPES)[number];
const CACHE_FINGERPRINT_PREFIXES = ["git:", "glob:", "glob!:", "file:", "env:"] as const;
/** Phase types that must NOT be cached across runs (a fresh result is required each run). */
const CACHE_CROSS_RUN_BLOCKED_TYPES = ["gate", "approval", "loop", "tournament", "script"] as const;
/** `cwd` is a literal path / workspace keyword, not an interpolated field. */
const CWD_PLACEHOLDER_RE = /\{[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\}/;

const ParallelTaskSchema = Type.Object(
{
Expand Down Expand Up @@ -649,6 +651,11 @@ export function validateTaskflow(def: unknown, opts: ValidationOptions = {}): Va
errors.push(`Phase '${p.id}': '${key}' must be a string, got ${typeof v}`);
}
}
if (typeof p.cwd === "string" && CWD_PLACEHOLDER_RE.test(p.cwd)) {
errors.push(
`Phase '${p.id}': 'cwd' does not support interpolation placeholders (${p.cwd}). Use a literal path, or a reserved workspace keyword ('temp', 'dedicated', 'worktree').`,
);
}
// dependsOn / from entries are string phase-id refs that flow into the graph
// helpers and nodeId(); a non-string entry would crash the renderer.
for (const key of ["dependsOn", "from"] as const) {
Expand Down
25 changes: 25 additions & 0 deletions packages/taskflow-core/test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@ test("validateTaskflow: new phase types and fields", () => {
);
});

test("validateTaskflow: cwd must be a literal path or workspace keyword (no interpolation)", () => {
const withArgs = validateTaskflow({
name: "x",
phases: [{ id: "p", type: "agent", task: "t", cwd: "{args.repo_dir}" }],
});
assert.equal(withArgs.ok, false);
assert.match(withArgs.errors.join("\n"), /cwd.*does not support interpolation placeholders/i);

const withSteps = validateTaskflow({
name: "x",
phases: [{ id: "p", type: "agent", task: "t", cwd: "{steps.plan.output}" }],
});
assert.equal(withSteps.ok, false);
assert.match(withSteps.errors.join("\n"), /cwd.*does not support interpolation placeholders/i);

assert.equal(
validateTaskflow({ name: "x", phases: [{ id: "p", type: "agent", task: "t", cwd: "./repo" }] }).ok,
true,
);
assert.equal(
validateTaskflow({ name: "x", phases: [{ id: "p", type: "agent", task: "t", cwd: "worktree" }] }).ok,
true,
);
});

test("validateTaskflow: duplicate ids and unknown deps", () => {
const dup = { name: "x", phases: [{ id: "p", type: "agent", task: "t" }, { id: "p", type: "agent", task: "t" }] };
assert.equal(validateTaskflow(dup).ok, false);
Expand Down