diff --git a/docs/src/pages/cli.astro b/docs/src/pages/cli.astro
index 9667cd7..cce308d 100644
--- a/docs/src/pages/cli.astro
+++ b/docs/src/pages/cli.astro
@@ -163,12 +163,17 @@ dex complete abc123 --result "Planning complete, no code changes" --no-commit`}
-d, --description — Updated description
--add-blocker <id> — Add blocking dependency
--remove-blocker <id> — Remove blocking dependency
+ --parent <id> — Move task under a new parent
+ --remove-parent — Promote a subtask to a top-level task
+ Note: --parent and --remove-parent cannot be used together.
diff --git a/plugins/dex/skills/dex/cli-reference.md b/plugins/dex/skills/dex/cli-reference.md
index da96c9e..f2314e3 100644
--- a/plugins/dex/skills/dex/cli-reference.md
+++ b/plugins/dex/skills/dex/cli-reference.md
@@ -67,8 +67,12 @@ This captures commit SHA, message, and branch automatically. The linked GitHub/S
dex edit -n "Updated name" --description "Updated description"
dex edit --add-blocker xyz123 # Add blocking dependency
dex edit --remove-blocker xyz123 # Remove blocking dependency
+dex edit --parent xyz123 # Move task under a new parent
+dex edit --remove-parent # Promote a subtask to a top-level task
```
+Note: `--parent` and `--remove-parent` cannot be used together.
+
## Delete a Task
```bash
diff --git a/src/cli/completion.test.ts b/src/cli/completion.test.ts
index 7aa359a..6cb83c8 100644
--- a/src/cli/completion.test.ts
+++ b/src/cli/completion.test.ts
@@ -52,6 +52,13 @@ describe("completion command", () => {
expect(out).toContain("complete");
expect(out).toContain("completion");
});
+
+ it("includes --remove-parent flag for edit command", async () => {
+ await runCli(["completion", "bash"], { storage });
+
+ const out = getStdout();
+ expect(out).toContain("--remove-parent");
+ });
});
describe("zsh completion", () => {
@@ -73,6 +80,15 @@ describe("completion command", () => {
expect(out).toContain("list:List tasks");
expect(out).toContain("show:View task details");
});
+
+ it("includes --remove-parent flag for edit command", async () => {
+ await runCli(["completion", "zsh"], { storage });
+
+ const out = getStdout();
+ expect(out).toContain(
+ "--remove-parent[Promote subtask to top-level task]",
+ );
+ });
});
describe("fish completion", () => {
@@ -93,6 +109,13 @@ describe("completion command", () => {
expect(out).toContain('-a "list" -d "List tasks"');
expect(out).toContain('-a "show" -d "View task details"');
});
+
+ it("includes --remove-parent flag for edit command", async () => {
+ await runCli(["completion", "fish"], { storage });
+
+ const out = getStdout();
+ expect(out).toContain("-l remove-parent");
+ });
});
describe("help", () => {
diff --git a/src/cli/completion/bash.ts b/src/cli/completion/bash.ts
index 4dd9cb6..bc6451d 100644
--- a/src/cli/completion/bash.ts
+++ b/src/cli/completion/bash.ts
@@ -63,7 +63,7 @@ _dex_completion() {
flags="--expand -e --full -f --json --help -h"
;;
edit|update)
- flags="--name -n --description -d --priority -p --parent --add-blocker --remove-blocker --help -h"
+ flags="--name -n --description -d --priority -p --parent --remove-parent --add-blocker --remove-blocker --help -h"
;;
complete|done)
flags="--result -r --commit -c --help -h"
diff --git a/src/cli/completion/fish.ts b/src/cli/completion/fish.ts
index 69c7749..e88e9c4 100644
--- a/src/cli/completion/fish.ts
+++ b/src/cli/completion/fish.ts
@@ -97,6 +97,7 @@ complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (
complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s d -l description -d "New description" -r
complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s p -l priority -d "New priority" -r
complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -l parent -d "New parent task ID" -r -a "(__dex_task_ids)"
+complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -l remove-parent -d "Promote subtask to top-level task"
complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s s -l status -d "New status" -r -a "pending completed"
complete -c dex -n "contains -- edit (commandline -opc); or contains -- update (commandline -opc)" -s h -l help -d "Show help"
diff --git a/src/cli/completion/zsh.ts b/src/cli/completion/zsh.ts
index 13719a9..169f2a1 100644
--- a/src/cli/completion/zsh.ts
+++ b/src/cli/completion/zsh.ts
@@ -65,6 +65,7 @@ _dex() {
'(-d --description)'{-d,--description}'[New description]:description:' \\
'(-p --priority)'{-p,--priority}'[New priority]:priority:' \\
'--parent[New parent task ID]:parent:_dex_task_ids' \\
+ '--remove-parent[Promote subtask to top-level task]' \\
'--add-blocker[Add blocker task IDs]:blockers:_dex_task_ids' \\
'--remove-blocker[Remove blocker task IDs]:blockers:_dex_task_ids' \\
'(-h --help)'{-h,--help}'[Show help]'
diff --git a/src/cli/edit.test.ts b/src/cli/edit.test.ts
index 794dbb8..30809f6 100644
--- a/src/cli/edit.test.ts
+++ b/src/cli/edit.test.ts
@@ -285,6 +285,7 @@ describe("edit command", () => {
expect(out).toContain("--description");
expect(out).toContain("--add-blocker");
expect(out).toContain("--remove-blocker");
+ expect(out).toContain("--remove-parent");
});
it("requires task ID", async () => {
@@ -292,6 +293,84 @@ describe("edit command", () => {
expect(output.stderr.join("\n")).toContain("Task ID is required");
});
+ it("fails if both --remove-parent and --parent is provided", async () => {
+ await expect(
+ runCli(
+ [
+ "edit",
+ "abc123", // taskId
+ "--remove-parent",
+ "--parent",
+ "def456", // parentId
+ ],
+ { storage },
+ ),
+ ).rejects.toThrow("process.exit");
+
+ const out = output.stderr.join("\n");
+ expect(out).toContain(
+ "You cannot both remove a parent and set a new parent.",
+ );
+ expect(out).toContain(
+ "Use --parent to overwrite the existing parent ID with another parent task.",
+ );
+ });
+
+ it("removes parent_id from task if --remove-parent is provided", async () => {
+ // Create parent task
+ await runCli(["create", "-n", "Test task", "--description", "ctx"], {
+ storage,
+ });
+ const parentTaskId = output.stdout.join("\n").match(TASK_ID_REGEX)?.[1];
+ output.stdout.length = 0;
+
+ // Create subtask
+ await runCli(
+ [
+ "create",
+ "-n",
+ "Test subtask",
+ "--description",
+ "ctx",
+ "--parent",
+ parentTaskId!,
+ ],
+ {
+ storage,
+ },
+ );
+ const taskId = output.stdout.join("\n").match(TASK_ID_REGEX)?.[1];
+ output.stdout.length = 0;
+
+ // Test the paren taks lists the subtask as a child
+ await runCli(["show", parentTaskId!, "--json"], { storage });
+ let showOut = output.stdout.join("\n");
+ expect(JSON.parse(showOut).children.includes(taskId!)).toBe(true);
+ output.stdout.length = 0;
+
+ // Test the task is a child of the parent task
+ await runCli(["show", taskId!], { storage });
+ showOut = output.stdout.join("\n");
+ expect(showOut).toContain(`View parent task: dex show ${parentTaskId!}`);
+ output.stdout.length = 0;
+
+ // Remove the parent
+ await runCli(["edit", taskId!, "--remove-parent"], { storage });
+ output.stdout.length = 0;
+
+ // Test the task no longer shows it's parent task
+ await runCli(["show", taskId!], { storage });
+ showOut = output.stdout.join("\n");
+ expect(showOut).not.toContain("View parent task");
+ output.stdout.length = 0;
+
+ // Test the parent task no longer lists the subtask as a child
+ await runCli(["show", parentTaskId!, "--json"], { storage });
+ showOut = output.stdout.join("\n");
+ expect(JSON.parse(showOut).children.includes(taskId!)).toBe(false);
+ output.stdout.length = 0;
+ });
+
it("links commit to task with --commit flag", async () => {
await runCli(["create", "-n", "Test task", "--description", "ctx"], {
storage,
diff --git a/src/cli/edit.ts b/src/cli/edit.ts
index d25ac67..3b36e05 100644
--- a/src/cli/edit.ts
+++ b/src/cli/edit.ts
@@ -23,6 +23,7 @@ export async function editCommand(
parent: { hasValue: true },
"add-blocker": { hasValue: true },
"remove-blocker": { hasValue: true },
+ "remove-parent": { hasValue: false },
commit: { short: "c", hasValue: true },
help: { short: "h", hasValue: false },
},
@@ -45,6 +46,7 @@ ${colors.bold}OPTIONS:${colors.reset}
--parent New parent task ID
--add-blocker Comma-separated task IDs to add as blockers
--remove-blocker Comma-separated task IDs to remove as blockers
+ --remove-parent Change the task from a subtask to a task
-c, --commit Link a git commit to the task
-h, --help Show this help message
@@ -54,6 +56,7 @@ ${colors.bold}EXAMPLE:${colors.reset}
dex edit abc123 --description "More details about the task"
dex edit abc123 --add-blocker def456
dex edit abc123 --remove-blocker def456
+ dex edit abc123 --remove-parent
dex edit abc123 --commit a1b2c3d
`);
return;
@@ -97,6 +100,20 @@ ${colors.bold}EXAMPLE:${colors.reset}
process.exit(1);
}
+ // Determine what parentId to assign to the task
+ const removeParent = getBooleanFlag(flags, "remove-parent");
+ const setParentId = getStringFlag(flags, "parent");
+ if (removeParent && setParentId) {
+ console.error(
+ `${colors.red}Error:${colors.reset} You cannot both remove a parent and set a new parent.`,
+ );
+ console.error(
+ ` Use --parent to overwrite the existing parent ID with another parent task.`,
+ );
+ process.exit(1);
+ }
+ const newParentId = removeParent ? null : setParentId;
+
const service = createService(options);
try {
// Fetch existing task to merge metadata
@@ -122,7 +139,7 @@ ${colors.bold}EXAMPLE:${colors.reset}
id,
name: getStringFlag(flags, "name"),
description: getStringFlag(flags, "description"),
- parent_id: getStringFlag(flags, "parent"),
+ parent_id: newParentId,
priority: parseIntFlag(flags, "priority"),
add_blocked_by: addBlockedBy,
remove_blocked_by: removeBlockedBy,