@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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/cli/block.js +114 -0
- package/dist/cli/context.js +139 -0
- package/dist/cli/done.js +98 -0
- package/dist/cli/edge.js +99 -0
- package/dist/cli/export.js +97 -0
- package/dist/cli/import.js +123 -0
- package/dist/cli/index.js +78 -0
- package/dist/cli/init.js +106 -0
- package/dist/cli/next.js +97 -0
- package/dist/cli/note.js +72 -0
- package/dist/cli/plan.js +108 -0
- package/dist/cli/portfolio.js +159 -0
- package/dist/cli/setup.js +142 -0
- package/dist/cli/show.js +142 -0
- package/dist/cli/split.js +191 -0
- package/dist/cli/start.js +94 -0
- package/dist/cli/status.js +149 -0
- package/dist/cli/task.js +92 -0
- package/dist/cli/utils.js +74 -0
- package/dist/db/commit.js +18 -0
- package/dist/db/connection.js +22 -0
- package/dist/db/escape.js +6 -0
- package/dist/db/migrate.js +159 -0
- package/dist/db/query.js +102 -0
- package/dist/domain/errors.js +33 -0
- package/dist/domain/invariants.js +103 -0
- package/dist/domain/types.js +120 -0
- package/dist/export/dot.js +21 -0
- package/dist/export/graph-data.js +41 -0
- package/dist/export/markdown.js +108 -0
- package/dist/export/mermaid.js +27 -0
- package/dist/plan-import/importer.js +155 -0
- package/dist/plan-import/parser.js +213 -0
- package/dist/template/.cursor/memory.md +14 -0
- package/dist/template/.cursor/rules/memory.mdc +11 -0
- package/dist/template/.cursor/rules/plan-authoring.mdc +42 -0
- package/dist/template/.cursor/rules/session-start.mdc +18 -0
- package/dist/template/.cursor/rules/taskgraph-workflow.mdc +35 -0
- package/dist/template/AGENT.md +73 -0
- package/dist/template/docs/backend.md +33 -0
- package/dist/template/docs/frontend.md +31 -0
- package/dist/template/docs/infra.md +26 -0
- package/dist/template/docs/skills/README.md +14 -0
- package/dist/template/docs/skills/plan-authoring.md +38 -0
- package/dist/template/docs/skills/refactoring-safely.md +21 -0
- package/dist/template/docs/skills/taskgraph-lifecycle-execution.md +23 -0
- 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
|
+
}
|
package/dist/cli/show.js
ADDED
|
@@ -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
|
+
}
|