@danalexilewis/taskgraph 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/cli/block.js +114 -0
  4. package/dist/cli/context.js +139 -0
  5. package/dist/cli/done.js +98 -0
  6. package/dist/cli/edge.js +99 -0
  7. package/dist/cli/export.js +97 -0
  8. package/dist/cli/import.js +123 -0
  9. package/dist/cli/index.js +78 -0
  10. package/dist/cli/init.js +106 -0
  11. package/dist/cli/next.js +97 -0
  12. package/dist/cli/note.js +72 -0
  13. package/dist/cli/plan.js +108 -0
  14. package/dist/cli/portfolio.js +159 -0
  15. package/dist/cli/setup.js +142 -0
  16. package/dist/cli/show.js +142 -0
  17. package/dist/cli/split.js +191 -0
  18. package/dist/cli/start.js +94 -0
  19. package/dist/cli/status.js +149 -0
  20. package/dist/cli/task.js +92 -0
  21. package/dist/cli/utils.js +74 -0
  22. package/dist/db/commit.js +18 -0
  23. package/dist/db/connection.js +22 -0
  24. package/dist/db/escape.js +6 -0
  25. package/dist/db/migrate.js +159 -0
  26. package/dist/db/query.js +102 -0
  27. package/dist/domain/errors.js +33 -0
  28. package/dist/domain/invariants.js +103 -0
  29. package/dist/domain/types.js +120 -0
  30. package/dist/export/dot.js +21 -0
  31. package/dist/export/graph-data.js +41 -0
  32. package/dist/export/markdown.js +108 -0
  33. package/dist/export/mermaid.js +27 -0
  34. package/dist/plan-import/importer.js +155 -0
  35. package/dist/plan-import/parser.js +213 -0
  36. package/dist/template/.cursor/memory.md +14 -0
  37. package/dist/template/.cursor/rules/memory.mdc +11 -0
  38. package/dist/template/.cursor/rules/plan-authoring.mdc +42 -0
  39. package/dist/template/.cursor/rules/session-start.mdc +18 -0
  40. package/dist/template/.cursor/rules/taskgraph-workflow.mdc +35 -0
  41. package/dist/template/AGENT.md +73 -0
  42. package/dist/template/docs/backend.md +33 -0
  43. package/dist/template/docs/frontend.md +31 -0
  44. package/dist/template/docs/infra.md +26 -0
  45. package/dist/template/docs/skills/README.md +14 -0
  46. package/dist/template/docs/skills/plan-authoring.md +38 -0
  47. package/dist/template/docs/skills/refactoring-safely.md +21 -0
  48. package/dist/template/docs/skills/taskgraph-lifecycle-execution.md +23 -0
  49. package/package.json +47 -0
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.portfolioCommand = portfolioCommand;
4
+ const commander_1 = require("commander");
5
+ const utils_1 = require("./utils"); // Import Config
6
+ const neverthrow_1 = require("neverthrow");
7
+ const errors_1 = require("../domain/errors");
8
+ const query_1 = require("../db/query");
9
+ function portfolioCommand(program) {
10
+ program
11
+ .command("portfolio")
12
+ .description("Analyze portfolio views")
13
+ .addCommand(portfolioOverlapsCommand())
14
+ .addCommand(portfolioHotspotsCommand());
15
+ }
16
+ function portfolioOverlapsCommand() {
17
+ return new commander_1.Command("overlaps")
18
+ .description("Find tasks shared by multiple feature_key")
19
+ .option("--min <count>", "Minimum number of features for overlap", "2")
20
+ .action(async (options, cmd) => {
21
+ const result = await (0, utils_1.readConfig)().asyncAndThen((config) => {
22
+ const q = (0, query_1.query)(config.doltRepoPath);
23
+ const minFeatures = parseInt(options.min, 10);
24
+ if (isNaN(minFeatures) || minFeatures <= 0) {
25
+ return (0, neverthrow_1.errAsync)((0, errors_1.buildError)(errors_1.ErrorCode.VALIDATION_FAILED, `Invalid min count: ${options.min}. Must be a positive integer.`));
26
+ }
27
+ const relatesOverlapsQuery = `
28
+ SELECT
29
+ t1.task_id, t1.title, t1.feature_key,
30
+ GROUP_CONCAT(DISTINCT t2.feature_key ORDER BY t2.feature_key) as related_features,
31
+ COUNT(DISTINCT t2.feature_key) as feature_count
32
+ FROM \`task\` t1
33
+ JOIN \`edge\` e ON t1.task_id = e.from_task_id AND e.type = 'relates'
34
+ JOIN \`task\` t2 ON e.to_task_id = t2.task_id
35
+ WHERE t1.feature_key IS NOT NULL AND t2.feature_key IS NOT NULL AND t1.feature_key != t2.feature_key
36
+ GROUP BY t1.task_id, t1.title, t1.feature_key
37
+ HAVING feature_count >= ${minFeatures} -1;
38
+ `;
39
+ const areaHotspotQuery = `
40
+ SELECT
41
+ area,
42
+ COUNT(DISTINCT task_id) as task_count,
43
+ GROUP_CONCAT(DISTINCT feature_key ORDER BY feature_key) as features_in_area,
44
+ COUNT(DISTINCT feature_key) as feature_key_count
45
+ FROM \`task\`
46
+ WHERE area IS NOT NULL
47
+ GROUP BY area
48
+ HAVING feature_key_count >= ${minFeatures};
49
+ `;
50
+ return q.raw(relatesOverlapsQuery).andThen((relatesOverlapsResult) => {
51
+ const relatesOverlaps = relatesOverlapsResult;
52
+ return q.raw(areaHotspotQuery).map((areaHotspotsResult) => {
53
+ const areaHotspots = areaHotspotsResult;
54
+ return { relatesOverlaps, areaHotspots };
55
+ });
56
+ });
57
+ });
58
+ result.match((data) => {
59
+ const resultData = data;
60
+ if (!cmd.parent?.opts().json) {
61
+ if (resultData.relatesOverlaps.length > 0) {
62
+ console.log(`\nTasks with explicit 'relates' overlaps (min ${options.min} features):`);
63
+ resultData.relatesOverlaps.forEach((overlap) => {
64
+ console.log(` - Task ID: ${overlap.task_id}, Title: ${overlap.title}, Features: ${overlap.feature_key}, ${overlap.related_features}`);
65
+ });
66
+ }
67
+ if (resultData.areaHotspots.length > 0) {
68
+ console.log(`\nAreas with tasks from multiple features (min ${options.min} features):`);
69
+ resultData.areaHotspots.forEach((hotspot) => {
70
+ console.log(` - Area: ${hotspot.area}, Tasks: ${hotspot.task_count}, Features: ${hotspot.features_in_area}`);
71
+ });
72
+ }
73
+ if (resultData.relatesOverlaps.length === 0 &&
74
+ resultData.areaHotspots.length === 0) {
75
+ console.log("No overlaps found based on current criteria.");
76
+ }
77
+ }
78
+ else {
79
+ console.log(JSON.stringify(resultData, null, 2));
80
+ }
81
+ }, (error) => {
82
+ console.error(`Error fetching portfolio overlaps: ${error.message}`);
83
+ if (cmd.parent?.opts().json) {
84
+ console.log(JSON.stringify({
85
+ status: "error",
86
+ code: error.code,
87
+ message: error.message,
88
+ cause: error.cause,
89
+ }));
90
+ }
91
+ process.exit(1);
92
+ });
93
+ });
94
+ }
95
+ function portfolioHotspotsCommand() {
96
+ return new commander_1.Command("hotspots")
97
+ .description("Counts tasks per area, plus tasks touched by multiple features")
98
+ .action(async (options, cmd) => {
99
+ const result = await (0, utils_1.readConfig)().asyncAndThen((config) => {
100
+ const q = (0, query_1.query)(config.doltRepoPath);
101
+ const tasksPerAreaQuery = `
102
+ SELECT area, COUNT(*) as task_count
103
+ FROM \`task\`
104
+ WHERE area IS NOT NULL
105
+ GROUP BY area
106
+ ORDER BY task_count DESC;
107
+ `;
108
+ const multiFeatureTasksQuery = `
109
+ SELECT task_id, title, GROUP_CONCAT(DISTINCT feature_key) as features
110
+ FROM \`task\`
111
+ WHERE feature_key IS NOT NULL
112
+ GROUP BY task_id, title
113
+ HAVING COUNT(DISTINCT feature_key) > 1;
114
+ `;
115
+ return q.raw(tasksPerAreaQuery).andThen((tasksPerAreaResult) => {
116
+ const tasksPerArea = tasksPerAreaResult;
117
+ return q.raw(multiFeatureTasksQuery).map((multiFeatureTasksResult) => {
118
+ const multiFeatureTasks = multiFeatureTasksResult;
119
+ return { tasksPerArea, multiFeatureTasks };
120
+ });
121
+ });
122
+ });
123
+ result.match((data) => {
124
+ const resultData = data;
125
+ if (!cmd.parent?.opts().json) {
126
+ if (resultData.tasksPerArea.length > 0) {
127
+ console.log("\nTasks per Area:");
128
+ resultData.tasksPerArea.forEach((item) => {
129
+ console.log(` - Area: ${item.area}, Tasks: ${item.task_count}`);
130
+ });
131
+ }
132
+ if (resultData.multiFeatureTasks.length > 0) {
133
+ console.log("\nTasks touched by multiple features:");
134
+ resultData.multiFeatureTasks.forEach((item) => {
135
+ console.log(` - Task ID: ${item.task_id}, Title: ${item.title}, Features: ${item.features}`);
136
+ });
137
+ }
138
+ if (resultData.tasksPerArea.length === 0 &&
139
+ resultData.multiFeatureTasks.length === 0) {
140
+ console.log("No hotspots found.");
141
+ }
142
+ }
143
+ else {
144
+ console.log(JSON.stringify(resultData, null, 2));
145
+ }
146
+ }, (error) => {
147
+ console.error(`Error fetching portfolio hotspots: ${error.message}`);
148
+ if (cmd.parent?.opts().json) {
149
+ console.log(JSON.stringify({
150
+ status: "error",
151
+ code: error.code,
152
+ message: error.message,
153
+ cause: error.cause,
154
+ }));
155
+ }
156
+ process.exit(1);
157
+ });
158
+ });
159
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.setupCommand = setupCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const neverthrow_1 = require("neverthrow");
40
+ const errors_1 = require("../domain/errors");
41
+ function copyTree(srcDir, destDir, repoRoot, options, result) {
42
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const srcPath = path.join(srcDir, entry.name);
45
+ const destPath = path.join(destDir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ if (!fs.existsSync(destPath)) {
48
+ fs.mkdirSync(destPath, { recursive: true });
49
+ }
50
+ copyTree(srcPath, destPath, repoRoot, options, result);
51
+ continue;
52
+ }
53
+ if (!entry.isFile())
54
+ continue;
55
+ const rel = path.relative(repoRoot, destPath);
56
+ if (fs.existsSync(destPath) && !options.force) {
57
+ result.skipped.push(rel);
58
+ continue;
59
+ }
60
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
61
+ fs.copyFileSync(srcPath, destPath);
62
+ result.created.push(rel);
63
+ }
64
+ }
65
+ function setupCommand(program) {
66
+ program
67
+ .command("setup")
68
+ .description("Scaffold repo conventions (docs/skills, example domain docs, and optional Cursor rules)")
69
+ .option("--no-docs", "Do not scaffold docs/ (domain docs + docs/skills)")
70
+ .option("--no-cursor", "Do not scaffold Cursor rules (.cursor/rules) and .cursor/memory.md")
71
+ .option("--force", "Overwrite existing files", false)
72
+ .action(async (rawOptions, cmd) => {
73
+ const raw = rawOptions;
74
+ const options = {
75
+ docs: raw.docs ?? true,
76
+ cursor: raw.cursor ?? true,
77
+ force: raw.force ?? false,
78
+ };
79
+ const repoRoot = process.cwd();
80
+ // At runtime this file is at dist/cli/setup.js; templates live at dist/template.
81
+ const templateRoot = path.join(__dirname, "..", "template");
82
+ const run = () => {
83
+ const result = { created: [], skipped: [] };
84
+ if (!fs.existsSync(templateRoot)) {
85
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.FILE_READ_FAILED, `Template directory missing: ${templateRoot}`);
86
+ }
87
+ if (options.docs) {
88
+ const docsSrc = path.join(templateRoot, "docs");
89
+ copyTree(docsSrc, path.join(repoRoot, "docs"), repoRoot, options, result);
90
+ }
91
+ if (options.cursor) {
92
+ const cursorSrc = path.join(templateRoot, ".cursor");
93
+ copyTree(cursorSrc, path.join(repoRoot, ".cursor"), repoRoot, options, result);
94
+ const agentMdSrc = path.join(templateRoot, "AGENT.md");
95
+ const agentMdDest = path.join(repoRoot, "AGENT.md");
96
+ if (fs.existsSync(agentMdSrc)) {
97
+ const rel = path.relative(repoRoot, agentMdDest);
98
+ if (fs.existsSync(agentMdDest) && !options.force) {
99
+ result.skipped.push(rel);
100
+ }
101
+ else {
102
+ fs.copyFileSync(agentMdSrc, agentMdDest);
103
+ result.created.push(rel);
104
+ }
105
+ }
106
+ }
107
+ const json = Boolean(cmd.parent?.opts().json);
108
+ if (json) {
109
+ console.log(JSON.stringify({
110
+ status: "ok",
111
+ created: result.created.sort(),
112
+ skipped: result.skipped.sort(),
113
+ }));
114
+ return;
115
+ }
116
+ console.log("TaskGraph scaffold complete.");
117
+ if (result.created.length > 0) {
118
+ console.log("Created:");
119
+ result.created.sort().forEach((p) => console.log(` + ${p}`));
120
+ }
121
+ if (result.skipped.length > 0) {
122
+ console.log("Skipped (already exists):");
123
+ result.skipped.sort().forEach((p) => console.log(` = ${p}`));
124
+ console.log("Tip: re-run with --force to overwrite.");
125
+ }
126
+ };
127
+ const res = await neverthrow_1.ResultAsync.fromPromise(Promise.resolve().then(run), (e) => (0, errors_1.buildError)(errors_1.ErrorCode.FILE_READ_FAILED, "Failed to scaffold repo files", e));
128
+ res.match(() => { }, (error) => {
129
+ const appError = error;
130
+ console.error(`Error: ${appError.message}`);
131
+ if (cmd.parent?.opts().json) {
132
+ console.log(JSON.stringify({
133
+ status: "error",
134
+ code: appError.code,
135
+ message: appError.message,
136
+ cause: appError.cause,
137
+ }));
138
+ }
139
+ process.exit(1);
140
+ });
141
+ });
142
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.showCommand = showCommand;
4
+ const utils_1 = require("./utils"); // Import Config
5
+ const neverthrow_1 = require("neverthrow");
6
+ const errors_1 = require("../domain/errors");
7
+ const query_1 = require("../db/query");
8
+ function showCommand(program) {
9
+ program
10
+ .command("show")
11
+ .description("Prints task details, blockers, dependents, and recent events")
12
+ .argument("<taskId>", "ID of the task to show")
13
+ .action(async (taskId, options, cmd) => {
14
+ const result = await (0, utils_1.readConfig)().asyncAndThen((config) => {
15
+ const q = (0, query_1.query)(config.doltRepoPath);
16
+ return neverthrow_1.ResultAsync.fromPromise((async () => {
17
+ const taskDetailQueryResult = await q.raw(`SELECT t.*, p.title as plan_title
18
+ FROM \`task\` t
19
+ JOIN \`plan\` p ON t.plan_id = p.plan_id
20
+ WHERE t.task_id = '${taskId}';`);
21
+ if (taskDetailQueryResult.isErr())
22
+ throw taskDetailQueryResult.error;
23
+ const taskDetailsArray = taskDetailQueryResult.value;
24
+ if (taskDetailsArray.length === 0) {
25
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.TASK_NOT_FOUND, `Task with ID ${taskId} not found.`);
26
+ }
27
+ const taskDetails = taskDetailsArray[0];
28
+ const blockersResult = await q.raw(`
29
+ SELECT e.from_task_id, t.title, t.status, e.reason
30
+ FROM \`edge\` e
31
+ JOIN \`task\` t ON e.from_task_id = t.task_id
32
+ WHERE e.to_task_id = '${taskId}' AND e.type = 'blocks';
33
+ `);
34
+ const blockers = blockersResult.isOk() ? blockersResult.value : [];
35
+ const dependentsResult = await q.raw(`
36
+ SELECT e.to_task_id, e.type, t.title, t.status, e.reason
37
+ FROM \`edge\` e
38
+ JOIN \`task\` t ON e.to_task_id = t.task_id
39
+ WHERE e.from_task_id = '${taskId}';
40
+ `);
41
+ const dependents = dependentsResult.isOk()
42
+ ? dependentsResult.value
43
+ : [];
44
+ const eventsResult = await q.raw(`
45
+ SELECT kind, body, created_at, actor
46
+ FROM \`event\`
47
+ WHERE task_id = '${taskId}'
48
+ ORDER BY created_at DESC
49
+ LIMIT 10;
50
+ `);
51
+ const events = eventsResult.isOk() ? eventsResult.value : [];
52
+ const noteEvents = events.filter((e) => e.kind === "note");
53
+ const domainsResult = await q.select("task_domain", { columns: ["domain"], where: { task_id: taskId } });
54
+ const skillsResult = await q.select("task_skill", { columns: ["skill"], where: { task_id: taskId } });
55
+ const domains = domainsResult.isOk()
56
+ ? domainsResult.value.map((r) => r.domain)
57
+ : [];
58
+ const skills = skillsResult.isOk()
59
+ ? skillsResult.value.map((r) => r.skill)
60
+ : [];
61
+ return {
62
+ taskDetails,
63
+ blockers,
64
+ dependents,
65
+ events,
66
+ noteEvents,
67
+ domains,
68
+ skills,
69
+ };
70
+ })(), (e) => e);
71
+ });
72
+ result.match((data) => {
73
+ const resultData = data;
74
+ if (!cmd.parent?.opts().json) {
75
+ const task = resultData.taskDetails;
76
+ console.log(`Task Details (ID: ${task.task_id}):`);
77
+ console.log(` Title: ${task.title}`);
78
+ console.log(` Plan: ${task.plan_title} (ID: ${task.plan_id})`);
79
+ console.log(` Status: ${task.status}`);
80
+ console.log(` Owner: ${task.owner}`);
81
+ console.log(` Area: ${task.area ?? "N/A"}`);
82
+ if (resultData.domains.length > 0)
83
+ console.log(` Domains: ${resultData.domains.join(", ")}`);
84
+ if (resultData.skills.length > 0)
85
+ console.log(` Skills: ${resultData.skills.join(", ")}`);
86
+ console.log(` Risk: ${task.risk}`);
87
+ console.log(` Estimate: ${task.estimate_mins ?? "N/A"} minutes`);
88
+ console.log(` Intent: ${task.intent ?? "N/A"}`);
89
+ console.log(` Scope In: ${task.scope_in ?? "N/A"}`);
90
+ console.log(` Scope Out: ${task.scope_out ?? "N/A"}`);
91
+ console.log(` Acceptance: ${task.acceptance ? JSON.stringify(task.acceptance) : "N/A"}`);
92
+ console.log(` Created At: ${task.created_at}`);
93
+ console.log(` Updated At: ${task.updated_at}`);
94
+ if (resultData.blockers.length > 0) {
95
+ console.log("\nBlockers:");
96
+ resultData.blockers.forEach((b) => {
97
+ console.log(` - Task ID: ${b.from_task_id}, Title: ${b.title}, Status: ${b.status}, Reason: ${b.reason ?? "N/A"}`);
98
+ });
99
+ }
100
+ if (resultData.dependents.length > 0) {
101
+ console.log("\nDependents:");
102
+ resultData.dependents.forEach((d) => {
103
+ const edge = d;
104
+ const typeInfo = edge.type ? `, Type: ${edge.type}` : "";
105
+ console.log(` - Task ID: ${edge.to_task_id}, Title: ${edge.title}, Status: ${edge.status}${typeInfo}, Reason: ${edge.reason ?? "N/A"}`);
106
+ });
107
+ }
108
+ if (resultData.noteEvents.length > 0) {
109
+ console.log("\nRecent Notes:");
110
+ resultData.noteEvents.slice(0, 5).forEach((e) => {
111
+ const body = typeof e.body === "string"
112
+ ? JSON.parse(e.body)
113
+ : e.body;
114
+ const msg = body?.message ?? JSON.stringify(e.body);
115
+ const agent = body?.agent ?? e.actor ?? "unknown";
116
+ console.log(` - [${e.created_at}] ${agent}: ${msg}`);
117
+ });
118
+ }
119
+ if (resultData.events.length > 0) {
120
+ console.log("\nRecent Events:");
121
+ resultData.events.forEach((e) => {
122
+ console.log(` - Kind: ${e.kind}, Actor: ${e.actor}, Created: ${e.created_at}, Body: ${JSON.stringify(e.body)}`);
123
+ });
124
+ }
125
+ }
126
+ else {
127
+ console.log(JSON.stringify(resultData, null, 2));
128
+ }
129
+ }, (error) => {
130
+ console.error(`Error showing task: ${error.message}`);
131
+ if (cmd.parent?.opts().json) {
132
+ console.log(JSON.stringify({
133
+ status: "error",
134
+ code: error.code,
135
+ message: error.message,
136
+ cause: error.cause,
137
+ }));
138
+ }
139
+ process.exit(1);
140
+ });
141
+ });
142
+ }
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitCommand = splitCommand;
4
+ const uuid_1 = require("uuid");
5
+ const commit_1 = require("../db/commit");
6
+ const utils_1 = require("./utils"); // Import Config
7
+ const types_1 = require("../domain/types");
8
+ const neverthrow_1 = require("neverthrow");
9
+ const errors_1 = require("../domain/errors");
10
+ const query_1 = require("../db/query");
11
+ function splitCommand(program) {
12
+ program
13
+ .command("split")
14
+ .description("Decompose a task into multiple subtasks")
15
+ .argument("<taskId>", "ID of the task to split")
16
+ .requiredOption("--into <titles>", "Pipe-separated titles of new subtasks (e.g., 'Task 1|Task 2')")
17
+ .option("--keep-original", "Keep the original task as a parent (default: true)", true)
18
+ .option("--link-direction <direction>", "Direction of the new edges (original-to-new or new-to-original)", "original-to-new")
19
+ .action(async (taskId, options, cmd) => {
20
+ const result = await (0, utils_1.readConfig)().asyncAndThen((config) => {
21
+ // Removed async, added type
22
+ const currentTimestamp = (0, query_1.now)();
23
+ return neverthrow_1.ResultAsync.fromPromise((async () => {
24
+ const q = (0, query_1.query)(config.doltRepoPath);
25
+ const originalTaskQueryResult = await q.select("task", {
26
+ where: { task_id: taskId },
27
+ });
28
+ if (originalTaskQueryResult.isErr())
29
+ throw originalTaskQueryResult.error;
30
+ const originalTasks = originalTaskQueryResult.value;
31
+ if (originalTasks.length === 0) {
32
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.TASK_NOT_FOUND, `Task with ID ${taskId} not found.`);
33
+ }
34
+ const originalTask = originalTasks[0];
35
+ const originalDomainsResult = await q.select("task_domain", { columns: ["domain"], where: { task_id: taskId } });
36
+ const originalSkillsResult = await q.select("task_skill", { columns: ["skill"], where: { task_id: taskId } });
37
+ const originalDomains = originalDomainsResult.isOk()
38
+ ? originalDomainsResult.value
39
+ : [];
40
+ const originalSkills = originalSkillsResult.isOk()
41
+ ? originalSkillsResult.value
42
+ : [];
43
+ const newTitles = options.into
44
+ .split("|")
45
+ .map((s) => s.trim());
46
+ const newTasks = [];
47
+ const taskMappings = [];
48
+ for (const title of newTitles) {
49
+ const newTaskId = (0, uuid_1.v4)();
50
+ const newTask = {
51
+ task_id: newTaskId,
52
+ plan_id: originalTask.plan_id,
53
+ feature_key: originalTask.feature_key,
54
+ title: title,
55
+ intent: originalTask.intent,
56
+ scope_in: originalTask.scope_in,
57
+ scope_out: originalTask.scope_out,
58
+ acceptance: originalTask.acceptance,
59
+ status: types_1.TaskStatusSchema.enum.todo, // New tasks start as todo
60
+ owner: originalTask.owner,
61
+ area: originalTask.area,
62
+ risk: originalTask.risk,
63
+ estimate_mins: null, // Estimate can be re-evaluated for subtasks
64
+ created_at: currentTimestamp,
65
+ updated_at: currentTimestamp,
66
+ external_key: null,
67
+ change_type: originalTask.change_type ?? null,
68
+ suggested_changes: originalTask.suggested_changes ?? null,
69
+ };
70
+ newTasks.push(newTask);
71
+ taskMappings.push({ original: taskId, new: newTaskId });
72
+ const insertTaskResult = await q.insert("task", {
73
+ task_id: newTask.task_id,
74
+ plan_id: newTask.plan_id,
75
+ feature_key: newTask.feature_key ?? null,
76
+ title: newTask.title,
77
+ intent: newTask.intent ?? null,
78
+ scope_in: newTask.scope_in ?? null,
79
+ scope_out: newTask.scope_out ?? null,
80
+ acceptance: newTask.acceptance
81
+ ? (0, query_1.jsonObj)({ val: JSON.stringify(newTask.acceptance) })
82
+ : null,
83
+ status: newTask.status,
84
+ owner: newTask.owner,
85
+ area: newTask.area ?? null,
86
+ risk: newTask.risk,
87
+ estimate_mins: newTask.estimate_mins ?? null,
88
+ created_at: newTask.created_at,
89
+ updated_at: newTask.updated_at,
90
+ external_key: newTask.external_key ?? null,
91
+ change_type: newTask.change_type ?? null,
92
+ suggested_changes: newTask.suggested_changes ?? null,
93
+ });
94
+ if (insertTaskResult.isErr())
95
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.DB_QUERY_FAILED, "Failed to insert new task", insertTaskResult.error);
96
+ const insertNewTaskEventResult = await q.insert("event", {
97
+ event_id: (0, uuid_1.v4)(),
98
+ task_id: newTask.task_id,
99
+ kind: "created",
100
+ body: (0, query_1.jsonObj)({ title: newTask.title, splitFrom: taskId }),
101
+ created_at: currentTimestamp,
102
+ });
103
+ if (insertNewTaskEventResult.isErr())
104
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.DB_QUERY_FAILED, "Failed to insert new task event", insertNewTaskEventResult.error);
105
+ for (const { domain } of originalDomains) {
106
+ const dr = await q.insert("task_domain", {
107
+ task_id: newTask.task_id,
108
+ domain,
109
+ });
110
+ if (dr.isErr())
111
+ throw dr.error;
112
+ }
113
+ for (const { skill } of originalSkills) {
114
+ const sr = await q.insert("task_skill", {
115
+ task_id: newTask.task_id,
116
+ skill,
117
+ });
118
+ if (sr.isErr())
119
+ throw sr.error;
120
+ }
121
+ let fromId = taskId;
122
+ let toId = newTask.task_id;
123
+ if (options.linkDirection === "new-to-original") {
124
+ fromId = newTask.task_id;
125
+ toId = taskId;
126
+ }
127
+ const insertEdgeResult = await q.insert("edge", {
128
+ from_task_id: fromId,
129
+ to_task_id: toId,
130
+ type: "relates",
131
+ reason: "split dependency",
132
+ });
133
+ if (insertEdgeResult.isErr())
134
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.DB_QUERY_FAILED, "Failed to insert new edge", insertEdgeResult.error);
135
+ }
136
+ if (!options.keepOriginal) {
137
+ const updateOriginalTaskResult = await q.update("task", { status: "canceled", updated_at: currentTimestamp }, { task_id: taskId });
138
+ if (updateOriginalTaskResult.isErr())
139
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.DB_QUERY_FAILED, "Failed to update original task", updateOriginalTaskResult.error);
140
+ }
141
+ const insertSplitEventResult = await q.insert("event", {
142
+ event_id: (0, uuid_1.v4)(),
143
+ task_id: taskId,
144
+ kind: "split",
145
+ body: (0, query_1.jsonObj)({
146
+ newTasks: newTasks.map((t) => ({
147
+ id: t.task_id,
148
+ title: t.title,
149
+ })),
150
+ taskMappings,
151
+ }),
152
+ created_at: currentTimestamp,
153
+ });
154
+ if (insertSplitEventResult.isErr())
155
+ throw (0, errors_1.buildError)(errors_1.ErrorCode.DB_QUERY_FAILED, "Failed to insert split event", insertSplitEventResult.error);
156
+ const commitResult = await (0, commit_1.doltCommit)(`task: split ${taskId} into ${newTitles.join(", ")}`, config.doltRepoPath, cmd.parent?.opts().noCommit);
157
+ if (commitResult.isErr())
158
+ throw commitResult.error;
159
+ return {
160
+ original_task_id: taskId,
161
+ new_tasks: newTasks.map((t) => ({
162
+ task_id: t.task_id,
163
+ title: t.title,
164
+ })),
165
+ status: options.keepOriginal ? originalTask.status : "canceled",
166
+ };
167
+ })(), (e) => e);
168
+ });
169
+ result.match((data) => {
170
+ const resultData = data;
171
+ if (!cmd.parent?.opts().json) {
172
+ console.log(`Task ${resultData.original_task_id} split into new tasks.`);
173
+ resultData.new_tasks.forEach((task) => console.log(` - ${task.title} (ID: ${task.task_id})`));
174
+ }
175
+ else {
176
+ console.log(JSON.stringify(resultData, null, 2));
177
+ }
178
+ }, (error) => {
179
+ console.error(`Error splitting task: ${error.message}`);
180
+ if (cmd.parent?.opts().json) {
181
+ console.log(JSON.stringify({
182
+ status: "error",
183
+ code: error.code,
184
+ message: error.message,
185
+ cause: error.cause,
186
+ }));
187
+ }
188
+ process.exit(1);
189
+ });
190
+ });
191
+ }