@kitsy/coop 2.1.2 → 2.2.1
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/dist/index.js +2006 -511
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs22 from "fs";
|
|
5
|
+
import path26 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
|
|
@@ -223,6 +223,9 @@ function listTrackFiles(root) {
|
|
|
223
223
|
function listDeliveryFiles(root) {
|
|
224
224
|
return walkFiles(path.join(ensureCoopInitialized(root), "deliveries"), /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
|
|
225
225
|
}
|
|
226
|
+
function loadTasks(root) {
|
|
227
|
+
return listTaskFiles(root).map((filePath) => parseTaskFile(filePath).task);
|
|
228
|
+
}
|
|
226
229
|
function findTaskFileById(root, id) {
|
|
227
230
|
const target = `${id}.md`.toLowerCase();
|
|
228
231
|
const match = listTaskFiles(root).find((filePath) => path.basename(filePath).toLowerCase() === target);
|
|
@@ -234,8 +237,8 @@ function findTaskFileById(root, id) {
|
|
|
234
237
|
function todayIsoDate() {
|
|
235
238
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
236
239
|
}
|
|
237
|
-
function normalizeIdPart(
|
|
238
|
-
const normalized =
|
|
240
|
+
function normalizeIdPart(input2, fallback, maxLength = 12) {
|
|
241
|
+
const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").replace(/-/g, "");
|
|
239
242
|
if (!normalized) return fallback;
|
|
240
243
|
return normalized.slice(0, maxLength);
|
|
241
244
|
}
|
|
@@ -271,24 +274,24 @@ function shortDateToken(now = /* @__PURE__ */ new Date()) {
|
|
|
271
274
|
function randomToken() {
|
|
272
275
|
return crypto.randomBytes(4).toString("hex").toUpperCase();
|
|
273
276
|
}
|
|
274
|
-
function sanitizeTemplateValue(
|
|
275
|
-
const normalized =
|
|
277
|
+
function sanitizeTemplateValue(input2, fallback = "X") {
|
|
278
|
+
const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
276
279
|
return normalized || fallback;
|
|
277
280
|
}
|
|
278
|
-
function sanitizeSemanticWord(
|
|
279
|
-
return
|
|
281
|
+
function sanitizeSemanticWord(input2) {
|
|
282
|
+
return input2.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
|
|
280
283
|
}
|
|
281
|
-
function semanticWords(
|
|
282
|
-
const rawWords =
|
|
284
|
+
function semanticWords(input2) {
|
|
285
|
+
const rawWords = input2.split(/[^a-zA-Z0-9]+/g).map(sanitizeSemanticWord).filter(Boolean);
|
|
283
286
|
if (rawWords.length <= 1) {
|
|
284
287
|
return rawWords;
|
|
285
288
|
}
|
|
286
289
|
const filtered = rawWords.filter((word) => !SEMANTIC_STOP_WORDS.has(word));
|
|
287
290
|
return filtered.length > 0 ? filtered : rawWords;
|
|
288
291
|
}
|
|
289
|
-
function semanticTitleToken(
|
|
292
|
+
function semanticTitleToken(input2, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
|
|
290
293
|
const safeMaxLength = Number.isFinite(maxLength) ? Math.max(4, Math.floor(maxLength)) : DEFAULT_TITLE_TOKEN_LENGTH;
|
|
291
|
-
const words = semanticWords(
|
|
294
|
+
const words = semanticWords(input2);
|
|
292
295
|
if (words.length === 0) {
|
|
293
296
|
return "UNTITLED";
|
|
294
297
|
}
|
|
@@ -449,27 +452,27 @@ function generateConfiguredId(root, existingIds, context) {
|
|
|
449
452
|
|
|
450
453
|
// src/utils/aliases.ts
|
|
451
454
|
var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
|
|
452
|
-
function toPosixPath(
|
|
453
|
-
return
|
|
455
|
+
function toPosixPath(input2) {
|
|
456
|
+
return input2.replace(/\\/g, "/");
|
|
454
457
|
}
|
|
455
458
|
function indexFilePath(root) {
|
|
456
459
|
const { indexDataFormat } = readCoopConfig(root);
|
|
457
460
|
const extension = indexDataFormat === "json" ? "json" : "yml";
|
|
458
461
|
return path2.join(ensureCoopInitialized(root), ".index", `aliases.${extension}`);
|
|
459
462
|
}
|
|
460
|
-
function normalizeAliasValue(
|
|
461
|
-
return
|
|
463
|
+
function normalizeAliasValue(input2) {
|
|
464
|
+
return input2.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
|
|
462
465
|
}
|
|
463
|
-
function normalizePatternValue(
|
|
464
|
-
return
|
|
466
|
+
function normalizePatternValue(input2) {
|
|
467
|
+
return input2.trim().toUpperCase().replace(/_/g, ".");
|
|
465
468
|
}
|
|
466
|
-
function normalizeAlias(
|
|
467
|
-
const normalized = normalizeAliasValue(
|
|
469
|
+
function normalizeAlias(input2) {
|
|
470
|
+
const normalized = normalizeAliasValue(input2);
|
|
468
471
|
if (!normalized) {
|
|
469
472
|
throw new Error("Alias cannot be empty.");
|
|
470
473
|
}
|
|
471
474
|
if (!ALIAS_PATTERN.test(normalized)) {
|
|
472
|
-
throw new Error(`Invalid alias '${
|
|
475
|
+
throw new Error(`Invalid alias '${input2}'. Use letters/numbers with '.' and '-'.`);
|
|
473
476
|
}
|
|
474
477
|
return normalized;
|
|
475
478
|
}
|
|
@@ -656,8 +659,8 @@ function updateTaskAliases(filePath, aliases) {
|
|
|
656
659
|
function updateIdeaAliases(filePath, aliases) {
|
|
657
660
|
const parsed = parseIdeaFile(filePath);
|
|
658
661
|
const nextRaw = { ...parsed.raw, aliases };
|
|
659
|
-
const
|
|
660
|
-
fs2.writeFileSync(filePath,
|
|
662
|
+
const output2 = stringifyFrontmatter(nextRaw, parsed.body);
|
|
663
|
+
fs2.writeFileSync(filePath, output2, "utf8");
|
|
661
664
|
}
|
|
662
665
|
function resolveFilePath(root, target) {
|
|
663
666
|
return path2.join(root, ...target.file.split("/"));
|
|
@@ -799,18 +802,18 @@ function registerAliasCommand(program) {
|
|
|
799
802
|
}
|
|
800
803
|
|
|
801
804
|
// src/utils/taskflow.ts
|
|
802
|
-
import
|
|
805
|
+
import path5 from "path";
|
|
803
806
|
import {
|
|
804
807
|
TaskStatus,
|
|
805
808
|
check_permission,
|
|
806
809
|
load_auth_config,
|
|
807
810
|
load_graph as load_graph2,
|
|
808
|
-
parseTaskFile as
|
|
811
|
+
parseTaskFile as parseTaskFile5,
|
|
809
812
|
run_plugins_for_event as run_plugins_for_event2,
|
|
810
813
|
schedule_next,
|
|
811
814
|
transition as transition2,
|
|
812
|
-
validateStructural as
|
|
813
|
-
writeTask as
|
|
815
|
+
validateStructural as validateStructural3,
|
|
816
|
+
writeTask as writeTask4
|
|
814
817
|
} from "@kitsy/coop-core";
|
|
815
818
|
|
|
816
819
|
// src/integrations/github.ts
|
|
@@ -829,8 +832,8 @@ import { Octokit } from "octokit";
|
|
|
829
832
|
function isObject(value) {
|
|
830
833
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
831
834
|
}
|
|
832
|
-
function sanitizeBranchPart(
|
|
833
|
-
const normalized =
|
|
835
|
+
function sanitizeBranchPart(input2, fallback) {
|
|
836
|
+
const normalized = input2.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
834
837
|
return normalized || fallback;
|
|
835
838
|
}
|
|
836
839
|
function readGitRemote(root) {
|
|
@@ -990,15 +993,15 @@ function toGitHubClient(config) {
|
|
|
990
993
|
baseUrl: config.apiBaseUrl
|
|
991
994
|
});
|
|
992
995
|
return {
|
|
993
|
-
async createPullRequest(
|
|
996
|
+
async createPullRequest(input2) {
|
|
994
997
|
const response = await octokit.rest.pulls.create({
|
|
995
|
-
owner:
|
|
996
|
-
repo:
|
|
997
|
-
title:
|
|
998
|
-
body:
|
|
999
|
-
head:
|
|
1000
|
-
base:
|
|
1001
|
-
draft:
|
|
998
|
+
owner: input2.owner,
|
|
999
|
+
repo: input2.repo,
|
|
1000
|
+
title: input2.title,
|
|
1001
|
+
body: input2.body,
|
|
1002
|
+
head: input2.head,
|
|
1003
|
+
base: input2.base,
|
|
1004
|
+
draft: input2.draft
|
|
1002
1005
|
});
|
|
1003
1006
|
return {
|
|
1004
1007
|
number: response.data.number,
|
|
@@ -1008,14 +1011,14 @@ function toGitHubClient(config) {
|
|
|
1008
1011
|
draft: response.data.draft
|
|
1009
1012
|
};
|
|
1010
1013
|
},
|
|
1011
|
-
async updatePullRequest(
|
|
1014
|
+
async updatePullRequest(input2) {
|
|
1012
1015
|
const response = await octokit.rest.pulls.update({
|
|
1013
|
-
owner:
|
|
1014
|
-
repo:
|
|
1015
|
-
pull_number:
|
|
1016
|
-
title:
|
|
1017
|
-
body:
|
|
1018
|
-
base:
|
|
1016
|
+
owner: input2.owner,
|
|
1017
|
+
repo: input2.repo,
|
|
1018
|
+
pull_number: input2.pull_number,
|
|
1019
|
+
title: input2.title,
|
|
1020
|
+
body: input2.body,
|
|
1021
|
+
base: input2.base
|
|
1019
1022
|
});
|
|
1020
1023
|
return {
|
|
1021
1024
|
number: response.data.number,
|
|
@@ -1025,11 +1028,11 @@ function toGitHubClient(config) {
|
|
|
1025
1028
|
draft: response.data.draft
|
|
1026
1029
|
};
|
|
1027
1030
|
},
|
|
1028
|
-
async getPullRequest(
|
|
1031
|
+
async getPullRequest(input2) {
|
|
1029
1032
|
const response = await octokit.rest.pulls.get({
|
|
1030
|
-
owner:
|
|
1031
|
-
repo:
|
|
1032
|
-
pull_number:
|
|
1033
|
+
owner: input2.owner,
|
|
1034
|
+
repo: input2.repo,
|
|
1035
|
+
pull_number: input2.pull_number
|
|
1033
1036
|
});
|
|
1034
1037
|
return {
|
|
1035
1038
|
number: response.data.number,
|
|
@@ -1039,12 +1042,12 @@ function toGitHubClient(config) {
|
|
|
1039
1042
|
draft: response.data.draft
|
|
1040
1043
|
};
|
|
1041
1044
|
},
|
|
1042
|
-
async mergePullRequest(
|
|
1045
|
+
async mergePullRequest(input2) {
|
|
1043
1046
|
const response = await octokit.rest.pulls.merge({
|
|
1044
|
-
owner:
|
|
1045
|
-
repo:
|
|
1046
|
-
pull_number:
|
|
1047
|
-
merge_method:
|
|
1047
|
+
owner: input2.owner,
|
|
1048
|
+
repo: input2.repo,
|
|
1049
|
+
pull_number: input2.pull_number,
|
|
1050
|
+
merge_method: input2.merge_method
|
|
1048
1051
|
});
|
|
1049
1052
|
return {
|
|
1050
1053
|
merged: response.data.merged,
|
|
@@ -1383,16 +1386,238 @@ function createGitHubWebhookServer(root, options = {}) {
|
|
|
1383
1386
|
return server;
|
|
1384
1387
|
}
|
|
1385
1388
|
|
|
1386
|
-
// src/utils/
|
|
1387
|
-
|
|
1389
|
+
// src/utils/work-items.ts
|
|
1390
|
+
import fs3 from "fs";
|
|
1391
|
+
import path3 from "path";
|
|
1392
|
+
import {
|
|
1393
|
+
effective_priority,
|
|
1394
|
+
parseDeliveryFile,
|
|
1395
|
+
parseIdeaFile as parseIdeaFile2,
|
|
1396
|
+
parseTaskFile as parseTaskFile4,
|
|
1397
|
+
stringifyFrontmatter as stringifyFrontmatter2,
|
|
1398
|
+
validateStructural as validateStructural2,
|
|
1399
|
+
validateSemantic,
|
|
1400
|
+
writeTask as writeTask3
|
|
1401
|
+
} from "@kitsy/coop-core";
|
|
1402
|
+
function resolveTaskFile(root, idOrAlias) {
|
|
1403
|
+
const reference = resolveReference(root, idOrAlias, "task");
|
|
1404
|
+
return path3.join(root, ...reference.file.split("/"));
|
|
1405
|
+
}
|
|
1406
|
+
function resolveIdeaFile(root, idOrAlias) {
|
|
1407
|
+
const reference = resolveReference(root, idOrAlias, "idea");
|
|
1408
|
+
return path3.join(root, ...reference.file.split("/"));
|
|
1409
|
+
}
|
|
1410
|
+
function loadTaskEntry(root, idOrAlias) {
|
|
1411
|
+
const filePath = resolveTaskFile(root, idOrAlias);
|
|
1412
|
+
return { filePath, parsed: parseTaskFile4(filePath) };
|
|
1413
|
+
}
|
|
1414
|
+
function loadIdeaEntry(root, idOrAlias) {
|
|
1415
|
+
const filePath = resolveIdeaFile(root, idOrAlias);
|
|
1416
|
+
return { filePath, parsed: parseIdeaFile2(filePath) };
|
|
1417
|
+
}
|
|
1418
|
+
function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
|
|
1419
|
+
const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
|
|
1420
|
+
fs3.writeFileSync(filePath, output2, "utf8");
|
|
1421
|
+
}
|
|
1422
|
+
function validateTaskForWrite(task, filePath) {
|
|
1423
|
+
const structuralIssues = validateStructural2(task, { filePath });
|
|
1424
|
+
const semanticIssues = validateSemantic(task);
|
|
1425
|
+
const errors = [...structuralIssues, ...semanticIssues].filter((issue) => issue.level === "error");
|
|
1426
|
+
if (errors.length > 0) {
|
|
1427
|
+
throw new Error(errors.map((issue) => `- ${issue.message}`).join("\n"));
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function writeTaskEntry(filePath, parsed, task, body = parsed.body) {
|
|
1431
|
+
validateTaskForWrite(task, filePath);
|
|
1432
|
+
writeTask3(task, {
|
|
1433
|
+
body,
|
|
1434
|
+
raw: parsed.raw,
|
|
1435
|
+
filePath
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
function appendTaskComment(task, author, body, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1439
|
+
return {
|
|
1440
|
+
...task,
|
|
1441
|
+
updated: todayIsoDate(),
|
|
1442
|
+
comments: [...task.comments ?? [], { at, author, body }]
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function appendTaskTimeLog(task, actor, kind, hours, note, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1446
|
+
const current = task.time ?? {};
|
|
1447
|
+
return {
|
|
1448
|
+
...task,
|
|
1449
|
+
updated: todayIsoDate(),
|
|
1450
|
+
time: {
|
|
1451
|
+
...current,
|
|
1452
|
+
logs: [...current.logs ?? [], { at, actor, kind, hours, note }]
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function setTaskPlannedHours(task, plannedHours) {
|
|
1457
|
+
return {
|
|
1458
|
+
...task,
|
|
1459
|
+
updated: todayIsoDate(),
|
|
1460
|
+
time: {
|
|
1461
|
+
...task.time ?? {},
|
|
1462
|
+
planned_hours: plannedHours
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function promoteTaskForContext(task, context) {
|
|
1467
|
+
const next = {
|
|
1468
|
+
...task,
|
|
1469
|
+
updated: todayIsoDate(),
|
|
1470
|
+
fix_versions: [...task.fix_versions ?? []],
|
|
1471
|
+
delivery_tracks: [...task.delivery_tracks ?? []],
|
|
1472
|
+
priority_context: { ...task.priority_context ?? {} }
|
|
1473
|
+
};
|
|
1474
|
+
const scopedTrack = context.track?.trim();
|
|
1475
|
+
if (scopedTrack) {
|
|
1476
|
+
if (task.track?.trim() !== scopedTrack && !next.delivery_tracks?.includes(scopedTrack)) {
|
|
1477
|
+
next.delivery_tracks = [...next.delivery_tracks ?? [], scopedTrack];
|
|
1478
|
+
}
|
|
1479
|
+
next.priority_context = {
|
|
1480
|
+
...next.priority_context ?? {},
|
|
1481
|
+
[scopedTrack]: "p0"
|
|
1482
|
+
};
|
|
1483
|
+
} else {
|
|
1484
|
+
next.priority = "p0";
|
|
1485
|
+
}
|
|
1486
|
+
const version = context.version?.trim();
|
|
1487
|
+
if (version && !next.fix_versions?.includes(version)) {
|
|
1488
|
+
next.fix_versions = [...next.fix_versions ?? [], version];
|
|
1489
|
+
}
|
|
1490
|
+
return next;
|
|
1491
|
+
}
|
|
1492
|
+
function taskEffectivePriority(task, track) {
|
|
1493
|
+
return effective_priority(task, track);
|
|
1494
|
+
}
|
|
1495
|
+
function resolveDeliveryEntry(root, ref) {
|
|
1496
|
+
const files = listDeliveryFiles(root);
|
|
1497
|
+
const target = ref.trim().toLowerCase();
|
|
1498
|
+
const entries = files.map((filePath) => {
|
|
1499
|
+
const parsed = parseDeliveryFile(filePath);
|
|
1500
|
+
return { filePath, delivery: parsed.delivery, body: parsed.body };
|
|
1501
|
+
});
|
|
1502
|
+
const direct = entries.find((entry) => entry.delivery.id.toLowerCase() === target);
|
|
1503
|
+
if (direct) return direct;
|
|
1504
|
+
const byName = entries.filter((entry) => entry.delivery.name.toLowerCase() === target);
|
|
1505
|
+
if (byName.length === 1) return byName[0];
|
|
1506
|
+
if (byName.length > 1) {
|
|
1507
|
+
throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id.`);
|
|
1508
|
+
}
|
|
1509
|
+
throw new Error(`Delivery '${ref}' not found.`);
|
|
1510
|
+
}
|
|
1511
|
+
function sharedDefault(root, key) {
|
|
1388
1512
|
const config = readCoopConfig(root).raw;
|
|
1389
1513
|
const defaults = typeof config.defaults === "object" && config.defaults !== null ? config.defaults : {};
|
|
1390
|
-
|
|
1514
|
+
const value = defaults[key];
|
|
1515
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/utils/working-context.ts
|
|
1519
|
+
import crypto3 from "crypto";
|
|
1520
|
+
import fs4 from "fs";
|
|
1521
|
+
import path4 from "path";
|
|
1522
|
+
function stableKey(root, projectId) {
|
|
1523
|
+
return crypto3.createHash("sha256").update(`${path4.resolve(root)}::${projectId}`).digest("hex").slice(0, 16);
|
|
1524
|
+
}
|
|
1525
|
+
function contextFilePath(root, coopHome) {
|
|
1526
|
+
const identity = readCoopIdentity(root);
|
|
1527
|
+
return path4.join(coopHome, "contexts", `${stableKey(root, identity.id)}.json`);
|
|
1528
|
+
}
|
|
1529
|
+
function readPersistedContext(root, coopHome) {
|
|
1530
|
+
const filePath = contextFilePath(root, coopHome);
|
|
1531
|
+
if (!fs4.existsSync(filePath)) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf8"));
|
|
1536
|
+
} catch {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function readWorkingContext(root, coopHome) {
|
|
1541
|
+
return readPersistedContext(root, coopHome)?.values ?? {};
|
|
1542
|
+
}
|
|
1543
|
+
function writeWorkingContext(root, coopHome, values) {
|
|
1544
|
+
const identity = readCoopIdentity(root);
|
|
1545
|
+
const filePath = contextFilePath(root, coopHome);
|
|
1546
|
+
fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
1547
|
+
const normalized = {
|
|
1548
|
+
track: values.track?.trim() || void 0,
|
|
1549
|
+
delivery: values.delivery?.trim() || void 0,
|
|
1550
|
+
version: values.version?.trim() || void 0
|
|
1551
|
+
};
|
|
1552
|
+
const payload = {
|
|
1553
|
+
repo_root: path4.resolve(root),
|
|
1554
|
+
project_id: identity.id,
|
|
1555
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1556
|
+
values: normalized
|
|
1557
|
+
};
|
|
1558
|
+
fs4.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}
|
|
1559
|
+
`, "utf8");
|
|
1560
|
+
return normalized;
|
|
1561
|
+
}
|
|
1562
|
+
function updateWorkingContext(root, coopHome, patch) {
|
|
1563
|
+
return writeWorkingContext(root, coopHome, { ...readWorkingContext(root, coopHome), ...patch });
|
|
1564
|
+
}
|
|
1565
|
+
function clearWorkingContext(root, coopHome, scope) {
|
|
1566
|
+
if (scope === "all") {
|
|
1567
|
+
const filePath = contextFilePath(root, coopHome);
|
|
1568
|
+
if (fs4.existsSync(filePath)) {
|
|
1569
|
+
fs4.rmSync(filePath, { force: true });
|
|
1570
|
+
}
|
|
1571
|
+
return {};
|
|
1572
|
+
}
|
|
1573
|
+
const next = { ...readWorkingContext(root, coopHome) };
|
|
1574
|
+
delete next[scope];
|
|
1575
|
+
return writeWorkingContext(root, coopHome, next);
|
|
1576
|
+
}
|
|
1577
|
+
function resolveContextValueWithSource(explicit, contextValue2, sharedDefault2) {
|
|
1578
|
+
if (explicit?.trim()) {
|
|
1579
|
+
return { value: explicit.trim(), source: "arg" };
|
|
1580
|
+
}
|
|
1581
|
+
if (contextValue2?.trim()) {
|
|
1582
|
+
return { value: contextValue2.trim(), source: "use" };
|
|
1583
|
+
}
|
|
1584
|
+
if (sharedDefault2?.trim()) {
|
|
1585
|
+
return { value: sharedDefault2.trim(), source: "config" };
|
|
1586
|
+
}
|
|
1587
|
+
return {};
|
|
1588
|
+
}
|
|
1589
|
+
function isVerboseRequested() {
|
|
1590
|
+
return process.argv.includes("--verbose");
|
|
1591
|
+
}
|
|
1592
|
+
function formatResolvedContextMessage(values) {
|
|
1593
|
+
const lines = [];
|
|
1594
|
+
for (const key of ["track", "delivery", "version"]) {
|
|
1595
|
+
const entry = values[key];
|
|
1596
|
+
if (!entry?.value || !entry.source || entry.source === "arg") {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
lines.push(`[COOP][context] ${key}=${entry.value} (from ${entry.source === "use" ? "`coop use`" : "config defaults"})`);
|
|
1600
|
+
}
|
|
1601
|
+
return lines;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/utils/taskflow.ts
|
|
1605
|
+
function configDefaultTrack(root) {
|
|
1606
|
+
return sharedDefault(root, "track");
|
|
1607
|
+
}
|
|
1608
|
+
function configDefaultDelivery(root) {
|
|
1609
|
+
return sharedDefault(root, "delivery");
|
|
1610
|
+
}
|
|
1611
|
+
function configDefaultVersion(root) {
|
|
1612
|
+
return sharedDefault(root, "version");
|
|
1391
1613
|
}
|
|
1392
1614
|
function resolveSelectionOptions(root, options) {
|
|
1615
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
1393
1616
|
return {
|
|
1394
1617
|
...options,
|
|
1395
|
-
track: options.track?.trim() || configDefaultTrack(root)
|
|
1618
|
+
track: options.track?.trim() || context.track?.trim() || configDefaultTrack(root),
|
|
1619
|
+
delivery: options.delivery?.trim() || context.delivery?.trim() || configDefaultDelivery(root),
|
|
1620
|
+
version: options.version?.trim() || context.version?.trim() || configDefaultVersion(root)
|
|
1396
1621
|
};
|
|
1397
1622
|
}
|
|
1398
1623
|
function selectTopReadyTask(root = resolveRepoRoot(), options = {}) {
|
|
@@ -1417,8 +1642,9 @@ function formatSelectedTask(entry, selection = {}) {
|
|
|
1417
1642
|
const lines = [
|
|
1418
1643
|
`Selected task: ${entry.task.id}`,
|
|
1419
1644
|
`Title: ${entry.task.title}`,
|
|
1420
|
-
`Priority: ${entry.task.priority ?? "-"}`,
|
|
1645
|
+
`Priority: ${selection.track && entry.task.priority_context?.[selection.track] ? `${entry.task.priority ?? "-"} -> ${entry.task.priority_context[selection.track]}` : entry.task.priority ?? "-"}`,
|
|
1421
1646
|
`Track: ${entry.task.track ?? "-"}`,
|
|
1647
|
+
`Delivery Tracks: ${entry.task.delivery_tracks && entry.task.delivery_tracks.length > 0 ? entry.task.delivery_tracks.join(", ") : "-"}`,
|
|
1422
1648
|
`Score: ${entry.score.toFixed(1)}`,
|
|
1423
1649
|
`Capacity fit: ${entry.fits_capacity ? "yes" : "no"}`,
|
|
1424
1650
|
`WIP fit: ${entry.fits_wip ? "yes" : "no"}`
|
|
@@ -1507,8 +1733,8 @@ async function assignTaskByReference(root, id, options) {
|
|
|
1507
1733
|
`Assignee '${assignee}' is not in resource profiles for track '${existing.track ?? "unassigned"}'. Known: ${known.join(", ")}.`
|
|
1508
1734
|
);
|
|
1509
1735
|
}
|
|
1510
|
-
const filePath =
|
|
1511
|
-
const parsed =
|
|
1736
|
+
const filePath = path5.join(root, ...reference.file.split("/"));
|
|
1737
|
+
const parsed = parseTaskFile5(filePath);
|
|
1512
1738
|
const from = parsed.task.assignee ?? null;
|
|
1513
1739
|
const actor = options.actor?.trim() || user;
|
|
1514
1740
|
if (from === assignee) {
|
|
@@ -1519,13 +1745,13 @@ async function assignTaskByReference(root, id, options) {
|
|
|
1519
1745
|
assignee,
|
|
1520
1746
|
updated: todayIsoDate()
|
|
1521
1747
|
};
|
|
1522
|
-
const structuralIssues =
|
|
1748
|
+
const structuralIssues = validateStructural3(nextTask, { filePath });
|
|
1523
1749
|
if (structuralIssues.length > 0) {
|
|
1524
1750
|
const errors = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
|
|
1525
1751
|
throw new Error(`Updated task is structurally invalid:
|
|
1526
1752
|
${errors}`);
|
|
1527
1753
|
}
|
|
1528
|
-
|
|
1754
|
+
writeTask4(nextTask, {
|
|
1529
1755
|
body: parsed.body,
|
|
1530
1756
|
raw: parsed.raw,
|
|
1531
1757
|
filePath
|
|
@@ -1582,8 +1808,8 @@ async function transitionTaskByReference(root, id, status, options) {
|
|
|
1582
1808
|
}
|
|
1583
1809
|
console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
|
|
1584
1810
|
}
|
|
1585
|
-
const filePath =
|
|
1586
|
-
const parsed =
|
|
1811
|
+
const filePath = path5.join(root, ...reference.file.split("/"));
|
|
1812
|
+
const parsed = parseTaskFile5(filePath);
|
|
1587
1813
|
const result = transition2(parsed.task, target, {
|
|
1588
1814
|
actor: options.actor ?? user,
|
|
1589
1815
|
dependencyStatuses: dependencyStatusMapForTask(reference.id, graph)
|
|
@@ -1591,13 +1817,13 @@ async function transitionTaskByReference(root, id, status, options) {
|
|
|
1591
1817
|
if (!result.success) {
|
|
1592
1818
|
throw new Error(result.error ?? "Transition failed.");
|
|
1593
1819
|
}
|
|
1594
|
-
const structuralIssues =
|
|
1820
|
+
const structuralIssues = validateStructural3(result.task, { filePath });
|
|
1595
1821
|
if (structuralIssues.length > 0) {
|
|
1596
1822
|
const errors = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
|
|
1597
1823
|
throw new Error(`Updated task is structurally invalid:
|
|
1598
1824
|
${errors}`);
|
|
1599
1825
|
}
|
|
1600
|
-
|
|
1826
|
+
writeTask4(result.task, {
|
|
1601
1827
|
body: parsed.body,
|
|
1602
1828
|
raw: parsed.raw,
|
|
1603
1829
|
filePath
|
|
@@ -1640,6 +1866,30 @@ function registerAssignCommand(program) {
|
|
|
1640
1866
|
});
|
|
1641
1867
|
}
|
|
1642
1868
|
|
|
1869
|
+
// src/utils/command-args.ts
|
|
1870
|
+
function resolveOptionalEntityArg(first, second, allowed, fallback) {
|
|
1871
|
+
const normalizedFirst = first.trim().toLowerCase();
|
|
1872
|
+
if (allowed.includes(normalizedFirst) && second?.trim()) {
|
|
1873
|
+
return { entity: normalizedFirst, id: second.trim() };
|
|
1874
|
+
}
|
|
1875
|
+
return { entity: fallback, id: first.trim() };
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/commands/comment.ts
|
|
1879
|
+
function registerCommentCommand(program) {
|
|
1880
|
+
program.command("comment").description("Append a comment to a COOP task").argument("<id-or-type>", "Task id/alias, or the literal `task` followed by an id").argument("[id]", "Task id when an explicit entity type is provided").requiredOption("--message <text>", "Comment text").option("--author <id>", "Comment author").action((first, second, options) => {
|
|
1881
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
1882
|
+
if (entity !== "task") {
|
|
1883
|
+
throw new Error("Only task comments are supported.");
|
|
1884
|
+
}
|
|
1885
|
+
const root = resolveRepoRoot();
|
|
1886
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
1887
|
+
const task = appendTaskComment(parsed.task, options.author?.trim() || defaultCoopAuthor(root), options.message.trim());
|
|
1888
|
+
writeTaskEntry(filePath, parsed, task);
|
|
1889
|
+
console.log(`Commented ${task.id}`);
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1643
1893
|
// src/commands/config.ts
|
|
1644
1894
|
var INDEX_DATA_KEY = "index.data";
|
|
1645
1895
|
var ID_NAMING_KEY = "id.naming";
|
|
@@ -1874,29 +2124,30 @@ function registerConfigCommand(program) {
|
|
|
1874
2124
|
}
|
|
1875
2125
|
|
|
1876
2126
|
// src/commands/create.ts
|
|
1877
|
-
import
|
|
1878
|
-
import
|
|
2127
|
+
import fs7 from "fs";
|
|
2128
|
+
import path8 from "path";
|
|
1879
2129
|
import {
|
|
1880
2130
|
DeliveryStatus,
|
|
1881
2131
|
IdeaStatus as IdeaStatus2,
|
|
1882
|
-
parseIdeaFile as
|
|
2132
|
+
parseIdeaFile as parseIdeaFile3,
|
|
1883
2133
|
TaskStatus as TaskStatus3,
|
|
1884
2134
|
TaskType as TaskType2,
|
|
1885
2135
|
check_permission as check_permission2,
|
|
1886
2136
|
load_auth_config as load_auth_config2,
|
|
1887
|
-
parseTaskFile as
|
|
2137
|
+
parseTaskFile as parseTaskFile7,
|
|
1888
2138
|
writeYamlFile as writeYamlFile3,
|
|
1889
|
-
stringifyFrontmatter as
|
|
1890
|
-
validateStructural as
|
|
1891
|
-
writeTask as
|
|
2139
|
+
stringifyFrontmatter as stringifyFrontmatter4,
|
|
2140
|
+
validateStructural as validateStructural5,
|
|
2141
|
+
writeTask as writeTask6
|
|
1892
2142
|
} from "@kitsy/coop-core";
|
|
1893
2143
|
import { create_provider_idea_decomposer, decompose_idea_to_tasks } from "@kitsy/coop-ai";
|
|
1894
2144
|
|
|
1895
2145
|
// src/utils/prompt.ts
|
|
1896
|
-
import readline from "readline
|
|
2146
|
+
import readline from "readline";
|
|
2147
|
+
import readlinePromises from "readline/promises";
|
|
1897
2148
|
import process2 from "process";
|
|
1898
2149
|
async function ask(question, defaultValue = "") {
|
|
1899
|
-
const rl =
|
|
2150
|
+
const rl = readlinePromises.createInterface({
|
|
1900
2151
|
input: process2.stdin,
|
|
1901
2152
|
output: process2.stdout
|
|
1902
2153
|
});
|
|
@@ -1905,15 +2156,80 @@ async function ask(question, defaultValue = "") {
|
|
|
1905
2156
|
rl.close();
|
|
1906
2157
|
return answer || defaultValue;
|
|
1907
2158
|
}
|
|
2159
|
+
function renderSelect(question, choices, selected) {
|
|
2160
|
+
process2.stdout.write(`${question}
|
|
2161
|
+
`);
|
|
2162
|
+
for (let index = 0; index < choices.length; index += 1) {
|
|
2163
|
+
const choice = choices[index];
|
|
2164
|
+
const prefix = index === selected ? ">" : " ";
|
|
2165
|
+
const hint = choice.hint ? ` - ${choice.hint}` : "";
|
|
2166
|
+
process2.stdout.write(` ${prefix} ${choice.label}${hint}
|
|
2167
|
+
`);
|
|
2168
|
+
}
|
|
2169
|
+
process2.stdout.write("\nUse \u2191/\u2193 to choose, Enter to confirm.\n");
|
|
2170
|
+
}
|
|
2171
|
+
function moveCursorUp(lines) {
|
|
2172
|
+
if (lines <= 0) return;
|
|
2173
|
+
readline.moveCursor(process2.stdout, 0, -lines);
|
|
2174
|
+
readline.clearScreenDown(process2.stdout);
|
|
2175
|
+
}
|
|
2176
|
+
async function select(question, choices, defaultIndex = 0) {
|
|
2177
|
+
if (choices.length === 0) {
|
|
2178
|
+
throw new Error(`No choices available for '${question}'.`);
|
|
2179
|
+
}
|
|
2180
|
+
if (!process2.stdin.isTTY || !process2.stdout.isTTY) {
|
|
2181
|
+
return choices[Math.min(Math.max(defaultIndex, 0), choices.length - 1)].value;
|
|
2182
|
+
}
|
|
2183
|
+
readline.emitKeypressEvents(process2.stdin);
|
|
2184
|
+
const previousRawMode = process2.stdin.isRaw;
|
|
2185
|
+
process2.stdin.setRawMode(true);
|
|
2186
|
+
let selected = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
|
|
2187
|
+
const renderedLines = choices.length + 2;
|
|
2188
|
+
renderSelect(question, choices, selected);
|
|
2189
|
+
return await new Promise((resolve, reject) => {
|
|
2190
|
+
const cleanup = () => {
|
|
2191
|
+
process2.stdin.off("keypress", onKeypress);
|
|
2192
|
+
process2.stdin.setRawMode(previousRawMode ?? false);
|
|
2193
|
+
process2.stdout.write("\n");
|
|
2194
|
+
};
|
|
2195
|
+
const rerender = () => {
|
|
2196
|
+
moveCursorUp(renderedLines + 1);
|
|
2197
|
+
renderSelect(question, choices, selected);
|
|
2198
|
+
};
|
|
2199
|
+
const onKeypress = (_input, key) => {
|
|
2200
|
+
if (key.name === "up") {
|
|
2201
|
+
selected = selected === 0 ? choices.length - 1 : selected - 1;
|
|
2202
|
+
rerender();
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (key.name === "down") {
|
|
2206
|
+
selected = selected === choices.length - 1 ? 0 : selected + 1;
|
|
2207
|
+
rerender();
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
if (key.name === "return") {
|
|
2211
|
+
const value = choices[selected].value;
|
|
2212
|
+
cleanup();
|
|
2213
|
+
resolve(value);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (key.ctrl && key.name === "c") {
|
|
2217
|
+
cleanup();
|
|
2218
|
+
reject(new Error("Prompt cancelled."));
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
process2.stdin.on("keypress", onKeypress);
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
1908
2224
|
|
|
1909
2225
|
// src/utils/idea-drafts.ts
|
|
1910
|
-
import
|
|
1911
|
-
import
|
|
2226
|
+
import fs5 from "fs";
|
|
2227
|
+
import path6 from "path";
|
|
1912
2228
|
import {
|
|
1913
2229
|
IdeaStatus,
|
|
1914
2230
|
parseFrontmatterContent,
|
|
1915
2231
|
parseYamlContent,
|
|
1916
|
-
stringifyFrontmatter as
|
|
2232
|
+
stringifyFrontmatter as stringifyFrontmatter3
|
|
1917
2233
|
} from "@kitsy/coop-core";
|
|
1918
2234
|
function asUniqueStrings(value) {
|
|
1919
2235
|
if (!Array.isArray(value)) return void 0;
|
|
@@ -1957,7 +2273,7 @@ function parseIdeaDraftInput(content, source) {
|
|
|
1957
2273
|
return parseIdeaDraftObject(parseYamlContent(content, source), source);
|
|
1958
2274
|
}
|
|
1959
2275
|
function writeIdeaFromDraft(root, projectDir, draft) {
|
|
1960
|
-
const existingIds = listIdeaFiles(root).map((filePath2) =>
|
|
2276
|
+
const existingIds = listIdeaFiles(root).map((filePath2) => path6.basename(filePath2, ".md"));
|
|
1961
2277
|
const id = draft.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
1962
2278
|
entityType: "idea",
|
|
1963
2279
|
title: draft.title,
|
|
@@ -1978,25 +2294,25 @@ function writeIdeaFromDraft(root, projectDir, draft) {
|
|
|
1978
2294
|
source: draft.source ?? "manual",
|
|
1979
2295
|
linked_tasks: draft.linked_tasks ?? []
|
|
1980
2296
|
};
|
|
1981
|
-
const filePath =
|
|
1982
|
-
if (
|
|
2297
|
+
const filePath = path6.join(projectDir, "ideas", `${id}.md`);
|
|
2298
|
+
if (fs5.existsSync(filePath)) {
|
|
1983
2299
|
throw new Error(`Idea '${id}' already exists.`);
|
|
1984
2300
|
}
|
|
1985
|
-
|
|
2301
|
+
fs5.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
|
|
1986
2302
|
return filePath;
|
|
1987
2303
|
}
|
|
1988
2304
|
|
|
1989
2305
|
// src/utils/refinement-drafts.ts
|
|
1990
|
-
import
|
|
1991
|
-
import
|
|
2306
|
+
import fs6 from "fs";
|
|
2307
|
+
import path7 from "path";
|
|
1992
2308
|
import {
|
|
1993
2309
|
IndexManager,
|
|
1994
2310
|
parseFrontmatterContent as parseFrontmatterContent2,
|
|
1995
|
-
parseTaskFile as
|
|
2311
|
+
parseTaskFile as parseTaskFile6,
|
|
1996
2312
|
parseYamlContent as parseYamlContent2,
|
|
1997
2313
|
stringifyYamlContent,
|
|
1998
|
-
validateStructural as
|
|
1999
|
-
writeTask as
|
|
2314
|
+
validateStructural as validateStructural4,
|
|
2315
|
+
writeTask as writeTask5
|
|
2000
2316
|
} from "@kitsy/coop-core";
|
|
2001
2317
|
|
|
2002
2318
|
// src/utils/stdin.ts
|
|
@@ -2023,16 +2339,16 @@ function nonEmptyStrings(value) {
|
|
|
2023
2339
|
return entries.length > 0 ? entries : void 0;
|
|
2024
2340
|
}
|
|
2025
2341
|
function refinementDir(projectDir) {
|
|
2026
|
-
const dir =
|
|
2027
|
-
|
|
2342
|
+
const dir = path7.join(projectDir, "tmp", "refinements");
|
|
2343
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2028
2344
|
return dir;
|
|
2029
2345
|
}
|
|
2030
2346
|
function draftPath(projectDir, mode, sourceId) {
|
|
2031
2347
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2032
|
-
return
|
|
2348
|
+
return path7.join(refinementDir(projectDir), `${mode}-${sourceId}-${stamp}.yml`);
|
|
2033
2349
|
}
|
|
2034
2350
|
function assignCreateProposalIds(root, draft) {
|
|
2035
|
-
const existingIds = listTaskFiles(root).map((filePath) =>
|
|
2351
|
+
const existingIds = listTaskFiles(root).map((filePath) => path7.basename(filePath, ".md"));
|
|
2036
2352
|
const createdIds = [];
|
|
2037
2353
|
const proposals = draft.proposals.map((proposal) => {
|
|
2038
2354
|
if (proposal.action !== "create") {
|
|
@@ -2061,13 +2377,13 @@ function assignCreateProposalIds(root, draft) {
|
|
|
2061
2377
|
};
|
|
2062
2378
|
}
|
|
2063
2379
|
function writeDraftFile(root, projectDir, draft, outputFile) {
|
|
2064
|
-
const filePath = outputFile?.trim() ?
|
|
2065
|
-
|
|
2066
|
-
|
|
2380
|
+
const filePath = outputFile?.trim() ? path7.resolve(root, outputFile.trim()) : draftPath(projectDir, draft.mode, draft.source.id);
|
|
2381
|
+
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
2382
|
+
fs6.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
|
|
2067
2383
|
return filePath;
|
|
2068
2384
|
}
|
|
2069
2385
|
function printDraftSummary(root, draft, filePath) {
|
|
2070
|
-
console.log(`[COOP] refinement draft created: ${
|
|
2386
|
+
console.log(`[COOP] refinement draft created: ${path7.relative(root, filePath)}`);
|
|
2071
2387
|
console.log(`[COOP] source: ${draft.source.entity_type} ${draft.source.id}`);
|
|
2072
2388
|
console.log(`[COOP] summary: ${draft.summary}`);
|
|
2073
2389
|
for (const proposal of draft.proposals) {
|
|
@@ -2076,7 +2392,7 @@ function printDraftSummary(root, draft, filePath) {
|
|
|
2076
2392
|
`- ${proposal.action.toUpperCase()} ${target ?? "(pending-id)"} | ${proposal.title} | ${proposal.type ?? "feature"} | ${proposal.priority ?? "p2"}`
|
|
2077
2393
|
);
|
|
2078
2394
|
}
|
|
2079
|
-
console.log(`[COOP] apply with: coop apply draft --from-file ${
|
|
2395
|
+
console.log(`[COOP] apply with: coop apply draft --from-file ${path7.relative(root, filePath)}`);
|
|
2080
2396
|
}
|
|
2081
2397
|
function parseRefinementDraftInput(content, source) {
|
|
2082
2398
|
const parsed = parseYamlContent2(content, source);
|
|
@@ -2160,16 +2476,16 @@ function applyCreateProposal(projectDir, proposal) {
|
|
|
2160
2476
|
if (!id) {
|
|
2161
2477
|
throw new Error(`Create proposal '${proposal.title}' is missing id.`);
|
|
2162
2478
|
}
|
|
2163
|
-
const filePath =
|
|
2164
|
-
if (
|
|
2479
|
+
const filePath = path7.join(projectDir, "tasks", `${id}.md`);
|
|
2480
|
+
if (fs6.existsSync(filePath)) {
|
|
2165
2481
|
throw new Error(`Task '${id}' already exists.`);
|
|
2166
2482
|
}
|
|
2167
2483
|
const task = taskFromProposal({ ...proposal, id }, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2168
|
-
const issues =
|
|
2484
|
+
const issues = validateStructural4(task, { filePath });
|
|
2169
2485
|
if (issues.length > 0) {
|
|
2170
2486
|
throw new Error(issues.map((issue) => issue.message).join(" | "));
|
|
2171
2487
|
}
|
|
2172
|
-
|
|
2488
|
+
writeTask5(task, { body: proposal.body ?? "", filePath });
|
|
2173
2489
|
return filePath;
|
|
2174
2490
|
}
|
|
2175
2491
|
function applyUpdateProposal(root, proposal) {
|
|
@@ -2178,7 +2494,7 @@ function applyUpdateProposal(root, proposal) {
|
|
|
2178
2494
|
throw new Error(`Update proposal '${proposal.title}' is missing target_id.`);
|
|
2179
2495
|
}
|
|
2180
2496
|
const filePath = findTaskFileById(root, targetId);
|
|
2181
|
-
const parsed =
|
|
2497
|
+
const parsed = parseTaskFile6(filePath);
|
|
2182
2498
|
const nextTask = {
|
|
2183
2499
|
...parsed.task,
|
|
2184
2500
|
title: proposal.title || parsed.task.title,
|
|
@@ -2196,11 +2512,11 @@ function applyUpdateProposal(root, proposal) {
|
|
|
2196
2512
|
derived_refs: proposal.derived_refs ?? parsed.task.origin?.derived_refs
|
|
2197
2513
|
} : parsed.task.origin
|
|
2198
2514
|
};
|
|
2199
|
-
const issues =
|
|
2515
|
+
const issues = validateStructural4(nextTask, { filePath });
|
|
2200
2516
|
if (issues.length > 0) {
|
|
2201
2517
|
throw new Error(issues.map((issue) => issue.message).join(" | "));
|
|
2202
2518
|
}
|
|
2203
|
-
|
|
2519
|
+
writeTask5(nextTask, {
|
|
2204
2520
|
body: proposal.body ?? parsed.body,
|
|
2205
2521
|
raw: parsed.raw,
|
|
2206
2522
|
filePath
|
|
@@ -2222,8 +2538,8 @@ function applyRefinementDraft(root, projectDir, draft) {
|
|
|
2222
2538
|
}
|
|
2223
2539
|
async function readDraftContent(root, options) {
|
|
2224
2540
|
if (options.fromFile?.trim()) {
|
|
2225
|
-
const filePath =
|
|
2226
|
-
return { content:
|
|
2541
|
+
const filePath = path7.resolve(root, options.fromFile.trim());
|
|
2542
|
+
return { content: fs6.readFileSync(filePath, "utf8"), source: filePath };
|
|
2227
2543
|
}
|
|
2228
2544
|
if (options.stdin) {
|
|
2229
2545
|
return { content: await readStdinText(), source: "<stdin>" };
|
|
@@ -2318,9 +2634,9 @@ function plusDaysIso(days) {
|
|
|
2318
2634
|
function unique(values) {
|
|
2319
2635
|
return Array.from(new Set(values));
|
|
2320
2636
|
}
|
|
2321
|
-
function
|
|
2637
|
+
function resolveIdeaFile2(root, idOrAlias) {
|
|
2322
2638
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
2323
|
-
return
|
|
2639
|
+
return path8.join(root, ...target.file.split("/"));
|
|
2324
2640
|
}
|
|
2325
2641
|
function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
2326
2642
|
const next = unique([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
|
|
@@ -2328,30 +2644,30 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
|
2328
2644
|
...raw,
|
|
2329
2645
|
linked_tasks: next
|
|
2330
2646
|
};
|
|
2331
|
-
|
|
2647
|
+
fs7.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
|
|
2332
2648
|
}
|
|
2333
|
-
function makeTaskDraft(
|
|
2649
|
+
function makeTaskDraft(input2) {
|
|
2334
2650
|
return {
|
|
2335
|
-
title:
|
|
2336
|
-
type:
|
|
2337
|
-
status:
|
|
2338
|
-
track:
|
|
2339
|
-
priority:
|
|
2340
|
-
body:
|
|
2341
|
-
acceptance: unique(
|
|
2342
|
-
testsRequired: unique(
|
|
2343
|
-
authorityRefs: unique(
|
|
2344
|
-
derivedRefs: unique(
|
|
2651
|
+
title: input2.title,
|
|
2652
|
+
type: input2.type,
|
|
2653
|
+
status: input2.status,
|
|
2654
|
+
track: input2.track,
|
|
2655
|
+
priority: input2.priority,
|
|
2656
|
+
body: input2.body,
|
|
2657
|
+
acceptance: unique(input2.acceptance ?? []),
|
|
2658
|
+
testsRequired: unique(input2.testsRequired ?? []),
|
|
2659
|
+
authorityRefs: unique(input2.authorityRefs ?? []),
|
|
2660
|
+
derivedRefs: unique(input2.derivedRefs ?? [])
|
|
2345
2661
|
};
|
|
2346
2662
|
}
|
|
2347
2663
|
function registerCreateCommand(program) {
|
|
2348
2664
|
const create = program.command("create").description("Create COOP entities");
|
|
2349
|
-
create.command("task").description("Create a task").argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "
|
|
2665
|
+
create.command("task").description("Create a task").argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
|
|
2350
2666
|
const root = resolveRepoRoot();
|
|
2351
2667
|
const coop = ensureCoopInitialized(root);
|
|
2352
2668
|
const interactive = Boolean(options.interactive);
|
|
2353
2669
|
if (options.fromFile?.trim() || options.stdin) {
|
|
2354
|
-
if (options.id || options.from || options.ai || options.title || titleArg || options.type || options.status || options.track || options.priority || options.body || (options.acceptance?.length ?? 0) > 0 || (options.testsRequired?.length ?? 0) > 0 || (options.authorityRef?.length ?? 0) > 0 || (options.derivedRef?.length ?? 0) > 0) {
|
|
2670
|
+
if (options.id || options.from || options.ai || options.title || titleArg || options.type || options.status || options.track || options.delivery || options.priority || options.body || (options.acceptance?.length ?? 0) > 0 || (options.testsRequired?.length ?? 0) > 0 || (options.authorityRef?.length ?? 0) > 0 || (options.derivedRef?.length ?? 0) > 0) {
|
|
2355
2671
|
throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
|
|
2356
2672
|
}
|
|
2357
2673
|
const draftInput = await readDraftContent(root, {
|
|
@@ -2365,7 +2681,7 @@ function registerCreateCommand(program) {
|
|
|
2365
2681
|
const written = applyRefinementDraft(root, coop, parsedDraft);
|
|
2366
2682
|
console.log(`[COOP] created ${written.length} task file(s) from ${draftInput.source}`);
|
|
2367
2683
|
for (const filePath of written) {
|
|
2368
|
-
console.log(`Created task: ${
|
|
2684
|
+
console.log(`Created task: ${path8.relative(root, filePath)}`);
|
|
2369
2685
|
}
|
|
2370
2686
|
return;
|
|
2371
2687
|
}
|
|
@@ -2373,6 +2689,7 @@ function registerCreateCommand(program) {
|
|
|
2373
2689
|
const statusInput = (options.status?.trim() || (interactive ? await ask("Task status", "todo") : "todo")).toLowerCase();
|
|
2374
2690
|
const track = options.track?.trim() || (interactive ? await ask("Track", "unassigned") : "unassigned");
|
|
2375
2691
|
const priority = options.priority?.trim() || (interactive ? await ask("Priority", "p2") : "p2");
|
|
2692
|
+
const delivery = options.delivery?.trim() || (interactive ? await ask("Delivery (optional)", "") : "");
|
|
2376
2693
|
const body = options.body ?? (interactive ? await ask("Task body (optional)", "") : "");
|
|
2377
2694
|
const acceptance = options.acceptance && options.acceptance.length > 0 ? unique(options.acceptance) : interactive ? parseCsv(await ask("Acceptance criteria (comma-separated, optional)", "")) : [];
|
|
2378
2695
|
const testsRequired = options.testsRequired && options.testsRequired.length > 0 ? unique(options.testsRequired) : interactive ? parseCsv(await ask("Tests required (comma-separated, optional)", "")) : [];
|
|
@@ -2383,8 +2700,8 @@ function registerCreateCommand(program) {
|
|
|
2383
2700
|
let sourceIdeaPath = null;
|
|
2384
2701
|
let sourceIdeaParsed = null;
|
|
2385
2702
|
if (options.from?.trim()) {
|
|
2386
|
-
sourceIdeaPath =
|
|
2387
|
-
sourceIdeaParsed =
|
|
2703
|
+
sourceIdeaPath = resolveIdeaFile2(root, options.from.trim());
|
|
2704
|
+
sourceIdeaParsed = parseIdeaFile3(sourceIdeaPath);
|
|
2388
2705
|
if (options.ai) {
|
|
2389
2706
|
const providerDecomposer = create_provider_idea_decomposer(readCoopConfig(root).raw);
|
|
2390
2707
|
const aiDrafts = await decompose_idea_to_tasks({
|
|
@@ -2456,7 +2773,7 @@ function registerCreateCommand(program) {
|
|
|
2456
2773
|
if (options.id && drafts.length > 1) {
|
|
2457
2774
|
throw new Error("Cannot combine --id with multi-task creation. Remove --id or disable --ai decomposition.");
|
|
2458
2775
|
}
|
|
2459
|
-
const existingIds = listTaskFiles(root).map((filePath) =>
|
|
2776
|
+
const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
|
|
2460
2777
|
const createdIds = [];
|
|
2461
2778
|
for (let index = 0; index < drafts.length; index += 1) {
|
|
2462
2779
|
const draft = drafts[index];
|
|
@@ -2483,6 +2800,7 @@ function registerCreateCommand(program) {
|
|
|
2483
2800
|
aliases: [],
|
|
2484
2801
|
track: draft.track,
|
|
2485
2802
|
priority: draft.priority,
|
|
2803
|
+
delivery: delivery || void 0,
|
|
2486
2804
|
acceptance: draft.acceptance,
|
|
2487
2805
|
tests_required: draft.testsRequired,
|
|
2488
2806
|
origin: draft.authorityRefs.length > 0 || draft.derivedRefs.length > 0 || options.from?.trim() ? {
|
|
@@ -2491,19 +2809,19 @@ function registerCreateCommand(program) {
|
|
|
2491
2809
|
promoted_from: options.from?.trim() ? [sourceIdeaParsed?.idea.id ?? options.from.trim()] : void 0
|
|
2492
2810
|
} : void 0
|
|
2493
2811
|
};
|
|
2494
|
-
const filePath =
|
|
2495
|
-
const structuralIssues =
|
|
2812
|
+
const filePath = path8.join(coop, "tasks", `${id}.md`);
|
|
2813
|
+
const structuralIssues = validateStructural5(task, { filePath });
|
|
2496
2814
|
if (structuralIssues.length > 0) {
|
|
2497
2815
|
const message = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
|
|
2498
2816
|
throw new Error(`Task failed structural validation:
|
|
2499
2817
|
${message}`);
|
|
2500
2818
|
}
|
|
2501
|
-
|
|
2819
|
+
writeTask6(task, {
|
|
2502
2820
|
body: draft.body,
|
|
2503
2821
|
filePath
|
|
2504
2822
|
});
|
|
2505
2823
|
createdIds.push(id);
|
|
2506
|
-
console.log(`Created task: ${
|
|
2824
|
+
console.log(`Created task: ${path8.relative(root, filePath)}`);
|
|
2507
2825
|
}
|
|
2508
2826
|
if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
|
|
2509
2827
|
updateIdeaLinkedTasks(
|
|
@@ -2530,7 +2848,7 @@ ${message}`);
|
|
|
2530
2848
|
});
|
|
2531
2849
|
const written = writeIdeaFromDraft(root, coop, parseIdeaDraftInput(draftInput.content, draftInput.source));
|
|
2532
2850
|
console.log(`[COOP] created 1 idea file from ${draftInput.source}`);
|
|
2533
|
-
console.log(`Created idea: ${
|
|
2851
|
+
console.log(`Created idea: ${path8.relative(root, written)}`);
|
|
2534
2852
|
return;
|
|
2535
2853
|
}
|
|
2536
2854
|
const title = options.title?.trim() || titleArg?.trim() || await ask("Idea title");
|
|
@@ -2540,7 +2858,7 @@ ${message}`);
|
|
|
2540
2858
|
const status = (options.status?.trim() || (interactive ? await ask("Idea status", "captured") : "captured")).toLowerCase();
|
|
2541
2859
|
const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
|
|
2542
2860
|
const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
|
|
2543
|
-
const existingIds = listIdeaFiles(root).map((filePath2) =>
|
|
2861
|
+
const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
|
|
2544
2862
|
const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
2545
2863
|
entityType: "idea",
|
|
2546
2864
|
title,
|
|
@@ -2564,9 +2882,9 @@ ${message}`);
|
|
|
2564
2882
|
if (!Object.values(IdeaStatus2).includes(status)) {
|
|
2565
2883
|
throw new Error(`Invalid idea status '${status}'.`);
|
|
2566
2884
|
}
|
|
2567
|
-
const filePath =
|
|
2568
|
-
|
|
2569
|
-
console.log(`Created idea: ${
|
|
2885
|
+
const filePath = path8.join(coop, "ideas", `${id}.md`);
|
|
2886
|
+
fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
|
|
2887
|
+
console.log(`Created idea: ${path8.relative(root, filePath)}`);
|
|
2570
2888
|
});
|
|
2571
2889
|
create.command("track").description("Create a track").argument("[name]", "Track name").option("--id <id>", "Track id").option("--name <name>", "Track name").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--max-wip <number>", "Max concurrent tasks").option("--allowed-types <types>", "Comma-separated allowed task types").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
2572
2890
|
const root = resolveRepoRoot();
|
|
@@ -2592,7 +2910,7 @@ ${message}`);
|
|
|
2592
2910
|
throw new Error(`Invalid task type in allowed-types: '${type}'.`);
|
|
2593
2911
|
}
|
|
2594
2912
|
}
|
|
2595
|
-
const existingIds = listTrackFiles(root).map((filePath2) =>
|
|
2913
|
+
const existingIds = listTrackFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml)$/i, ""));
|
|
2596
2914
|
const config = readCoopConfig(root);
|
|
2597
2915
|
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
2598
2916
|
const prefix = typeof idPrefixesRaw.track === "string" ? idPrefixesRaw.track : "TRK";
|
|
@@ -2613,9 +2931,9 @@ ${message}`);
|
|
|
2613
2931
|
allowed_types: allowed
|
|
2614
2932
|
}
|
|
2615
2933
|
};
|
|
2616
|
-
const filePath =
|
|
2934
|
+
const filePath = path8.join(coop, "tracks", `${id}.yml`);
|
|
2617
2935
|
writeYamlFile3(filePath, payload);
|
|
2618
|
-
console.log(`Created track: ${
|
|
2936
|
+
console.log(`Created track: ${path8.relative(root, filePath)}`);
|
|
2619
2937
|
});
|
|
2620
2938
|
create.command("delivery").description("Create a delivery").argument("[name]", "Delivery name").option("--id <id>", "Delivery id").option("--name <name>", "Delivery name").option("--status <status>", `Delivery status (${Object.values(DeliveryStatus).join(", ")})`).option("--commit", "Create delivery directly in committed state").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--budget-hours <hours>", "Engineering budget hours").option("--budget-cost <usd>", "Cost budget in USD").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--scope <ids>", "Comma-separated task ids to include in scope").option("--exclude <ids>", "Comma-separated task ids to exclude from scope").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
2621
2939
|
const root = resolveRepoRoot();
|
|
@@ -2668,7 +2986,7 @@ ${message}`);
|
|
|
2668
2986
|
options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
|
|
2669
2987
|
);
|
|
2670
2988
|
const tasks = listTaskFiles(root).map((filePath2) => {
|
|
2671
|
-
const parsed =
|
|
2989
|
+
const parsed = parseTaskFile7(filePath2).task;
|
|
2672
2990
|
return { id: parsed.id, title: parsed.title };
|
|
2673
2991
|
});
|
|
2674
2992
|
if (interactive && tasks.length > 0) {
|
|
@@ -2697,7 +3015,7 @@ ${message}`);
|
|
|
2697
3015
|
}
|
|
2698
3016
|
}
|
|
2699
3017
|
const existingIds = listDeliveryFiles(root).map(
|
|
2700
|
-
(filePath2) =>
|
|
3018
|
+
(filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, "")
|
|
2701
3019
|
);
|
|
2702
3020
|
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
2703
3021
|
const prefix = typeof idPrefixesRaw.delivery === "string" ? idPrefixesRaw.delivery : "DEL";
|
|
@@ -2726,9 +3044,113 @@ ${message}`);
|
|
|
2726
3044
|
exclude: scopeExclude
|
|
2727
3045
|
}
|
|
2728
3046
|
};
|
|
2729
|
-
const filePath =
|
|
3047
|
+
const filePath = path8.join(coop, "deliveries", `${id}.yml`);
|
|
2730
3048
|
writeYamlFile3(filePath, payload);
|
|
2731
|
-
console.log(`Created delivery: ${
|
|
3049
|
+
console.log(`Created delivery: ${path8.relative(root, filePath)}`);
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// src/commands/current.ts
|
|
3054
|
+
function contextValue(value) {
|
|
3055
|
+
return value?.trim() || "unset";
|
|
3056
|
+
}
|
|
3057
|
+
function selectionScope(context) {
|
|
3058
|
+
if (context.delivery?.trim()) {
|
|
3059
|
+
return `delivery '${context.delivery.trim()}'`;
|
|
3060
|
+
}
|
|
3061
|
+
if (context.track?.trim()) {
|
|
3062
|
+
return `track '${context.track.trim()}'`;
|
|
3063
|
+
}
|
|
3064
|
+
return "workspace-wide (no working track or delivery set)";
|
|
3065
|
+
}
|
|
3066
|
+
function selectionReason(context, score) {
|
|
3067
|
+
if (context.delivery?.trim()) {
|
|
3068
|
+
return `top ready task for delivery '${context.delivery.trim()}' with score ${score.toFixed(1)}`;
|
|
3069
|
+
}
|
|
3070
|
+
if (context.track?.trim()) {
|
|
3071
|
+
return `top ready task for track '${context.track.trim()}' with score ${score.toFixed(1)}`;
|
|
3072
|
+
}
|
|
3073
|
+
return `top ready task across the workspace with score ${score.toFixed(1)}`;
|
|
3074
|
+
}
|
|
3075
|
+
function registerCurrentCommand(program) {
|
|
3076
|
+
program.command("current").description("Show active project, working context, my in-progress tasks, and the next ready task").action(() => {
|
|
3077
|
+
const root = resolveRepoRoot();
|
|
3078
|
+
const identity = readCoopIdentity(root);
|
|
3079
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
3080
|
+
const actor = defaultCoopAuthor(root);
|
|
3081
|
+
const inProgress = loadTasks(root).filter(
|
|
3082
|
+
(task) => task.assignee === actor && (task.status === "in_progress" || task.status === "in_review")
|
|
3083
|
+
);
|
|
3084
|
+
console.log(`Project: ${identity.name} (${identity.id})`);
|
|
3085
|
+
console.log(`Actor: ${actor}`);
|
|
3086
|
+
console.log("");
|
|
3087
|
+
console.log("Working Context:");
|
|
3088
|
+
console.log(`- Track: ${contextValue(context.track)}`);
|
|
3089
|
+
console.log(`- Delivery: ${contextValue(context.delivery)}`);
|
|
3090
|
+
console.log(`- Version: ${contextValue(context.version)}`);
|
|
3091
|
+
if (!context.track?.trim() && !context.delivery?.trim()) {
|
|
3092
|
+
console.log("- Hint: use `coop use track <id>` and/or `coop use delivery <id>` to set your working context.");
|
|
3093
|
+
}
|
|
3094
|
+
console.log("");
|
|
3095
|
+
console.log("My Active Tasks:");
|
|
3096
|
+
if (inProgress.length === 0) {
|
|
3097
|
+
console.log("- none");
|
|
3098
|
+
} else {
|
|
3099
|
+
for (const task of inProgress) {
|
|
3100
|
+
console.log(`- ${task.id} [${task.status}] ${task.title}`);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
console.log("");
|
|
3104
|
+
console.log("Next Ready:");
|
|
3105
|
+
try {
|
|
3106
|
+
const selected = selectTopReadyTask(root, {
|
|
3107
|
+
track: context.track,
|
|
3108
|
+
delivery: context.delivery,
|
|
3109
|
+
version: context.version
|
|
3110
|
+
});
|
|
3111
|
+
console.log(`Selection scope: ${selectionScope(context)}`);
|
|
3112
|
+
console.log(`Why selected: ${selectionReason(context, selected.entry.score)}`);
|
|
3113
|
+
if ((selected.entry.task.track ?? "").trim().toLowerCase() === "unassigned") {
|
|
3114
|
+
console.log("Warning: selected task has no assigned track.");
|
|
3115
|
+
}
|
|
3116
|
+
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// src/commands/deps.ts
|
|
3124
|
+
import { load_graph as load_graph3 } from "@kitsy/coop-core";
|
|
3125
|
+
function registerDepsCommand(program) {
|
|
3126
|
+
program.command("deps").description("Show dependencies and reverse dependencies for a task, including status and title").argument("<id>", "Task id or alias").action((id) => {
|
|
3127
|
+
const root = resolveRepoRoot();
|
|
3128
|
+
const graph = load_graph3(coopDir(root));
|
|
3129
|
+
const reference = resolveReference(root, id, "task");
|
|
3130
|
+
const task = graph.nodes.get(reference.id);
|
|
3131
|
+
if (!task) {
|
|
3132
|
+
throw new Error(`Task '${reference.id}' not found.`);
|
|
3133
|
+
}
|
|
3134
|
+
const reverse = Array.from(graph.reverse.get(task.id) ?? []).sort((a, b) => a.localeCompare(b));
|
|
3135
|
+
console.log(`Task: ${task.id} [${task.status}] ${task.title}`);
|
|
3136
|
+
console.log("Depends On:");
|
|
3137
|
+
if (!task.depends_on || task.depends_on.length === 0) {
|
|
3138
|
+
console.log("- none");
|
|
3139
|
+
} else {
|
|
3140
|
+
for (const depId of task.depends_on) {
|
|
3141
|
+
const dep = graph.nodes.get(depId);
|
|
3142
|
+
console.log(`- ${depId}${dep ? ` [${dep.status}] ${dep.title}` : " [missing]"}`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
console.log("Required By:");
|
|
3146
|
+
if (reverse.length === 0) {
|
|
3147
|
+
console.log("- none");
|
|
3148
|
+
} else {
|
|
3149
|
+
for (const dependentId of reverse) {
|
|
3150
|
+
const dependent = graph.nodes.get(dependentId);
|
|
3151
|
+
console.log(`- ${dependentId}${dependent ? ` [${dependent.status}] ${dependent.title}` : " [missing]"}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
2732
3154
|
});
|
|
2733
3155
|
}
|
|
2734
3156
|
|
|
@@ -2737,7 +3159,8 @@ import chalk from "chalk";
|
|
|
2737
3159
|
import {
|
|
2738
3160
|
compute_critical_path,
|
|
2739
3161
|
compute_readiness_with_corrections,
|
|
2740
|
-
|
|
3162
|
+
effective_priority as effective_priority2,
|
|
3163
|
+
load_graph as load_graph4,
|
|
2741
3164
|
schedule_next as schedule_next2,
|
|
2742
3165
|
topological_sort,
|
|
2743
3166
|
validate_graph
|
|
@@ -2747,10 +3170,10 @@ import {
|
|
|
2747
3170
|
function normalize(value) {
|
|
2748
3171
|
return value.trim().toLowerCase();
|
|
2749
3172
|
}
|
|
2750
|
-
function resolveDelivery(graph,
|
|
2751
|
-
const direct = graph.deliveries.get(
|
|
3173
|
+
function resolveDelivery(graph, input2) {
|
|
3174
|
+
const direct = graph.deliveries.get(input2);
|
|
2752
3175
|
if (direct) return direct;
|
|
2753
|
-
const target = normalize(
|
|
3176
|
+
const target = normalize(input2);
|
|
2754
3177
|
const byId = Array.from(graph.deliveries.values()).find((delivery) => normalize(delivery.id) === target);
|
|
2755
3178
|
if (byId) return byId;
|
|
2756
3179
|
const byName = Array.from(graph.deliveries.values()).filter((delivery) => normalize(delivery.name) === target);
|
|
@@ -2758,9 +3181,9 @@ function resolveDelivery(graph, input3) {
|
|
|
2758
3181
|
return byName[0];
|
|
2759
3182
|
}
|
|
2760
3183
|
if (byName.length > 1) {
|
|
2761
|
-
throw new Error(`Multiple deliveries match '${
|
|
3184
|
+
throw new Error(`Multiple deliveries match '${input2}'. Use delivery id instead.`);
|
|
2762
3185
|
}
|
|
2763
|
-
throw new Error(`Delivery '${
|
|
3186
|
+
throw new Error(`Delivery '${input2}' not found.`);
|
|
2764
3187
|
}
|
|
2765
3188
|
|
|
2766
3189
|
// src/commands/graph.ts
|
|
@@ -2790,7 +3213,7 @@ function renderAsciiDag(tasks, order) {
|
|
|
2790
3213
|
}
|
|
2791
3214
|
function runValidate() {
|
|
2792
3215
|
const root = resolveRepoRoot();
|
|
2793
|
-
const graph =
|
|
3216
|
+
const graph = load_graph4(coopDir(root));
|
|
2794
3217
|
const issues = validate_graph(graph);
|
|
2795
3218
|
if (issues.length === 0) {
|
|
2796
3219
|
console.log(chalk.green("Graph is healthy. No invariant violations found."));
|
|
@@ -2805,12 +3228,20 @@ function runValidate() {
|
|
|
2805
3228
|
}
|
|
2806
3229
|
function runNext(options) {
|
|
2807
3230
|
const root = resolveRepoRoot();
|
|
2808
|
-
const
|
|
3231
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
3232
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track"));
|
|
3233
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
3234
|
+
if (isVerboseRequested()) {
|
|
3235
|
+
for (const line of formatResolvedContextMessage({ track: resolvedTrack, delivery: resolvedDelivery })) {
|
|
3236
|
+
console.log(line);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
const graph = load_graph4(coopDir(root));
|
|
2809
3240
|
const readiness = compute_readiness_with_corrections(graph);
|
|
2810
3241
|
const limit = options.limit && options.limit.trim().length > 0 ? Number(options.limit) : void 0;
|
|
2811
3242
|
const ready = schedule_next2(graph, {
|
|
2812
|
-
track:
|
|
2813
|
-
delivery:
|
|
3243
|
+
track: resolvedTrack.value,
|
|
3244
|
+
delivery: resolvedDelivery.value,
|
|
2814
3245
|
executor: options.executor,
|
|
2815
3246
|
today: options.today,
|
|
2816
3247
|
limit: Number.isInteger(limit) && Number(limit) > 0 ? Number(limit) : void 0
|
|
@@ -2824,7 +3255,7 @@ function runNext(options) {
|
|
|
2824
3255
|
ready.map((entry) => [
|
|
2825
3256
|
entry.task.id,
|
|
2826
3257
|
entry.task.title,
|
|
2827
|
-
entry.task.
|
|
3258
|
+
effective_priority2(entry.task, resolvedTrack.value),
|
|
2828
3259
|
entry.task.track ?? "-",
|
|
2829
3260
|
entry.score.toFixed(1),
|
|
2830
3261
|
entry.fits_capacity ? chalk.green("yes") : chalk.yellow("no"),
|
|
@@ -2854,13 +3285,13 @@ Warnings (${readiness.warnings.length}):`);
|
|
|
2854
3285
|
}
|
|
2855
3286
|
function runShow() {
|
|
2856
3287
|
const root = resolveRepoRoot();
|
|
2857
|
-
const graph =
|
|
3288
|
+
const graph = load_graph4(coopDir(root));
|
|
2858
3289
|
const order = topological_sort(graph);
|
|
2859
3290
|
console.log(renderAsciiDag(graph.nodes, order));
|
|
2860
3291
|
}
|
|
2861
3292
|
function runCriticalPath(deliveryName) {
|
|
2862
3293
|
const root = resolveRepoRoot();
|
|
2863
|
-
const graph =
|
|
3294
|
+
const graph = load_graph4(coopDir(root));
|
|
2864
3295
|
const delivery = resolveDelivery(graph, deliveryName);
|
|
2865
3296
|
const result = compute_critical_path(delivery, graph);
|
|
2866
3297
|
console.log(`Critical Path: ${delivery.name} (${delivery.id})`);
|
|
@@ -2899,13 +3330,16 @@ function registerGraphCommand(program) {
|
|
|
2899
3330
|
}
|
|
2900
3331
|
|
|
2901
3332
|
// src/utils/ai-help.ts
|
|
2902
|
-
import
|
|
3333
|
+
import path9 from "path";
|
|
2903
3334
|
var catalog = {
|
|
2904
3335
|
purpose: "COOP is a Git-native planning, backlog, execution, and orchestration CLI. It stores canonical data in .coop/projects/<project.id>/ and should be treated as the source of truth for work selection and lifecycle state.",
|
|
2905
3336
|
selection_rules: [
|
|
2906
3337
|
"Use `coop project show` first to confirm the active workspace and project.",
|
|
3338
|
+
"Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
|
|
2907
3339
|
"Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
|
|
2908
|
-
"
|
|
3340
|
+
"Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
|
|
3341
|
+
"Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
|
|
3342
|
+
"Use `coop show <id>` or `coop show task <id>` before implementation to read acceptance, tests_required, dependencies, origin refs, and task metadata.",
|
|
2909
3343
|
"Use `coop refine idea` or `coop refine task` when the task lacks planning detail. COOP owns canonical writes; agents should not edit `.coop` files directly."
|
|
2910
3344
|
],
|
|
2911
3345
|
workspace_rules: [
|
|
@@ -2955,6 +3389,11 @@ var catalog = {
|
|
|
2955
3389
|
{ usage: "coop project list", purpose: "List projects in the current workspace." },
|
|
2956
3390
|
{ usage: "coop project show", purpose: "Show the active project id, name, path, and layout." },
|
|
2957
3391
|
{ usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." },
|
|
3392
|
+
{ usage: "coop use show", purpose: "Show the user-local working defaults for track, delivery, and version." },
|
|
3393
|
+
{ usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
|
|
3394
|
+
{ usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
|
|
3395
|
+
{ usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
|
|
3396
|
+
{ usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
|
|
2958
3397
|
{ usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
|
|
2959
3398
|
{ usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
|
|
2960
3399
|
]
|
|
@@ -2967,6 +3406,7 @@ var catalog = {
|
|
|
2967
3406
|
{ usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
|
|
2968
3407
|
{ usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
|
|
2969
3408
|
{ usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
|
|
3409
|
+
{ usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
|
|
2970
3410
|
{
|
|
2971
3411
|
usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
|
|
2972
3412
|
purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
|
|
@@ -2993,8 +3433,10 @@ var catalog = {
|
|
|
2993
3433
|
commands: [
|
|
2994
3434
|
{ usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
|
|
2995
3435
|
{ usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
|
|
3436
|
+
{ usage: "coop pick task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Select a specific task, optionally promote it in the current context, assign it, and move it to in_progress." },
|
|
2996
3437
|
{ usage: "coop pick task --delivery MVP --claim --actor dev1 --user lead-user", purpose: "Select the top ready task, optionally assign it, and move it to in_progress." },
|
|
2997
|
-
{ usage: "coop start task PM-101 --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
|
|
3438
|
+
{ usage: "coop start task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
|
|
3439
|
+
{ usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context." },
|
|
2998
3440
|
{ usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
|
|
2999
3441
|
{ usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
|
|
3000
3442
|
{ usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
|
|
@@ -3008,8 +3450,21 @@ var catalog = {
|
|
|
3008
3450
|
description: "Read backlog state, task details, and planning output.",
|
|
3009
3451
|
commands: [
|
|
3010
3452
|
{ usage: "coop list tasks --status todo", purpose: "List tasks with filters." },
|
|
3453
|
+
{ usage: "coop list tasks --track MVP --delivery MVP --ready --columns id,title,p,assignee,score", purpose: "List ready tasks with lean columns and score visible." },
|
|
3454
|
+
{ usage: "coop list tasks --mine", purpose: "List tasks assigned to the current default COOP author." },
|
|
3455
|
+
{ usage: 'coop search "auth and login form"', purpose: "Run deterministic non-AI search across tasks, ideas, and deliveries." },
|
|
3456
|
+
{ usage: 'coop search "auth" --open', purpose: "Require a single match and print the resolved summary row." },
|
|
3457
|
+
{ usage: "coop show PM-101", purpose: "Resolve a task, idea, or delivery by reference without an extra entity noun." },
|
|
3458
|
+
{ usage: "coop show PM-101 --compact", purpose: "Show a smaller summary view for a large task." },
|
|
3011
3459
|
{ usage: "coop show task PM-101", purpose: "Show a task with acceptance, tests_required, refs, and runbook sections." },
|
|
3012
3460
|
{ usage: "coop show idea IDEA-101", purpose: "Show an idea." },
|
|
3461
|
+
{ usage: "coop deps PM-101", purpose: "Show task dependencies and reverse dependencies with status and title." },
|
|
3462
|
+
{ usage: "coop prompt PM-101 --format markdown", purpose: "Generate a manual handoff prompt from a task and current working context." },
|
|
3463
|
+
{ usage: "coop update PM-101 --track MVP --delivery MVP", purpose: "Update a task's home track or primary delivery without editing `.coop` files directly." },
|
|
3464
|
+
{ usage: "coop update PM-101 --add-delivery-track MVP --priority-in MVP:p0", purpose: "Add a contributing track lens and scoped priority override." },
|
|
3465
|
+
{ usage: "coop update PM-101 --priority p1 --add-fix-version v2", purpose: "Update task metadata without editing `.coop` files directly." },
|
|
3466
|
+
{ usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
|
|
3467
|
+
{ usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
|
|
3013
3468
|
{ usage: "coop plan delivery MVP", purpose: "Run delivery feasibility analysis." },
|
|
3014
3469
|
{ usage: "coop plan delivery MVP --monte-carlo --iterations 5000", purpose: "Run probabilistic delivery forecasting." },
|
|
3015
3470
|
{ usage: "coop view velocity", purpose: "Show historical throughput." },
|
|
@@ -3052,6 +3507,7 @@ var catalog = {
|
|
|
3052
3507
|
execution_model: [
|
|
3053
3508
|
"Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
|
|
3054
3509
|
"Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
|
|
3510
|
+
"Use `coop update`, `coop comment`, and `coop log-time` for task mutations instead of manually editing task files.",
|
|
3055
3511
|
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
|
|
3056
3512
|
"If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
|
|
3057
3513
|
],
|
|
@@ -3168,7 +3624,7 @@ function renderAiHelpTopic(format, topic) {
|
|
|
3168
3624
|
`;
|
|
3169
3625
|
}
|
|
3170
3626
|
function normalizeRepoPath(repoPath) {
|
|
3171
|
-
return repoPath ?
|
|
3627
|
+
return repoPath ? path9.resolve(repoPath) : process.cwd();
|
|
3172
3628
|
}
|
|
3173
3629
|
function formatSelectionCommand(commandName, delivery, track) {
|
|
3174
3630
|
if (delivery) {
|
|
@@ -3209,12 +3665,15 @@ function renderInitialPrompt(options = {}) {
|
|
|
3209
3665
|
`- If you are unsure about lifecycle changes, run \`${commandName} help-ai --state-transitions --format markdown\`.`,
|
|
3210
3666
|
`- If you are unsure where artifacts should go, run \`${commandName} help-ai --artifacts --format markdown\`.`,
|
|
3211
3667
|
`- If you are unsure whether to continue after a task, run \`${commandName} help-ai --post-execution --format markdown\`.`,
|
|
3668
|
+
`- Use \`${commandName} use show\` to inspect current working track, delivery, and version defaults before selecting work.`,
|
|
3212
3669
|
"- Use only commands that actually exist in COOP. Do not invent command names.",
|
|
3213
3670
|
"- Do not reprioritize work outside COOP unless the user explicitly overrides it.",
|
|
3214
3671
|
"- Select only the first ready task from the COOP readiness output.",
|
|
3215
|
-
"- Inspect the selected task with `coop show
|
|
3672
|
+
"- Inspect the selected task with `coop show <id>` before implementation.",
|
|
3216
3673
|
"- Do not create, edit, move, or normalize files directly inside `.coop/`; use COOP commands, MCP, or API surfaces so COOP remains the canonical writer.",
|
|
3217
3674
|
"- Respect lifecycle prerequisites: use `coop review task <id>` before `coop complete task <id>`; do not complete directly from `in_progress`.",
|
|
3675
|
+
"- Use `coop promote <id>` or `coop pick/start --promote` when the selected task must be escalated inside the active track/version context.",
|
|
3676
|
+
"- Use `coop update <id>`, `coop comment <id>`, and `coop log-time <id>` for task metadata changes; do not hand-edit task frontmatter.",
|
|
3218
3677
|
`- Write any contract-review, audit, or planning artifact under \`${artifactsDir}\` unless the user explicitly chooses another location.`
|
|
3219
3678
|
];
|
|
3220
3679
|
if (rigour === "strict") {
|
|
@@ -3256,6 +3715,9 @@ function renderAiHelp(format) {
|
|
|
3256
3715
|
const heading = format === "markdown" ? "# COOP AI Help" : "COOP AI Help";
|
|
3257
3716
|
lines.push(heading, "");
|
|
3258
3717
|
lines.push(catalog.purpose, "");
|
|
3718
|
+
lines.push("Fast agent handoff:");
|
|
3719
|
+
lines.push("- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`");
|
|
3720
|
+
lines.push("");
|
|
3259
3721
|
const bullet = (value) => format === "markdown" ? `- ${value}` : `- ${value}`;
|
|
3260
3722
|
lines.push(format === "markdown" ? "## Selection Rules" : "Selection Rules");
|
|
3261
3723
|
for (const rule of catalog.selection_rules) {
|
|
@@ -3341,7 +3803,7 @@ function registerHelpAiCommand(program) {
|
|
|
3341
3803
|
} catch {
|
|
3342
3804
|
artifactsDir = "docs";
|
|
3343
3805
|
}
|
|
3344
|
-
const
|
|
3806
|
+
const output2 = options.initialPrompt ? renderAiInitialPrompt({
|
|
3345
3807
|
repoPath: options.repo,
|
|
3346
3808
|
delivery: options.delivery,
|
|
3347
3809
|
track: options.track,
|
|
@@ -3349,7 +3811,7 @@ function registerHelpAiCommand(program) {
|
|
|
3349
3811
|
rigour,
|
|
3350
3812
|
artifactsDir
|
|
3351
3813
|
}) : topic ? renderAiHelpTopic(format, topic) : renderAiHelp(format);
|
|
3352
|
-
console.log(
|
|
3814
|
+
console.log(output2.trimEnd());
|
|
3353
3815
|
});
|
|
3354
3816
|
}
|
|
3355
3817
|
function resolveHelpTopic(options) {
|
|
@@ -3372,11 +3834,11 @@ function resolveHelpTopic(options) {
|
|
|
3372
3834
|
if (options.naming) {
|
|
3373
3835
|
requestedTopics.push("naming");
|
|
3374
3836
|
}
|
|
3375
|
-
const
|
|
3376
|
-
if (
|
|
3377
|
-
throw new Error(`Specify only one focused help-ai topic at a time. Received: ${
|
|
3837
|
+
const unique3 = [...new Set(requestedTopics)];
|
|
3838
|
+
if (unique3.length > 1) {
|
|
3839
|
+
throw new Error(`Specify only one focused help-ai topic at a time. Received: ${unique3.join(", ")}.`);
|
|
3378
3840
|
}
|
|
3379
|
-
const topic =
|
|
3841
|
+
const topic = unique3[0];
|
|
3380
3842
|
if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
|
|
3381
3843
|
throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
|
|
3382
3844
|
}
|
|
@@ -3384,7 +3846,7 @@ function resolveHelpTopic(options) {
|
|
|
3384
3846
|
}
|
|
3385
3847
|
|
|
3386
3848
|
// src/commands/index.ts
|
|
3387
|
-
import
|
|
3849
|
+
import path10 from "path";
|
|
3388
3850
|
import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
|
|
3389
3851
|
function runStatus(options) {
|
|
3390
3852
|
const root = resolveRepoRoot();
|
|
@@ -3394,7 +3856,7 @@ function runStatus(options) {
|
|
|
3394
3856
|
const freshness = status.stale ? "stale" : "fresh";
|
|
3395
3857
|
const existsText = status.exists ? "present" : "missing";
|
|
3396
3858
|
console.log(`[COOP] index ${existsText}, ${freshness}`);
|
|
3397
|
-
console.log(`[COOP] graph: ${
|
|
3859
|
+
console.log(`[COOP] graph: ${path10.relative(root, status.graph_path)}`);
|
|
3398
3860
|
if (status.generated_at) {
|
|
3399
3861
|
console.log(`[COOP] generated_at: ${status.generated_at}`);
|
|
3400
3862
|
}
|
|
@@ -3417,7 +3879,7 @@ function runRebuild() {
|
|
|
3417
3879
|
const graph = manager.build_full_index();
|
|
3418
3880
|
const elapsed = Date.now() - start;
|
|
3419
3881
|
console.log(`[COOP] index rebuilt: ${graph.nodes.size} tasks (${elapsed} ms)`);
|
|
3420
|
-
console.log(`[COOP] graph: ${
|
|
3882
|
+
console.log(`[COOP] graph: ${path10.relative(root, manager.graphPath)}`);
|
|
3421
3883
|
}
|
|
3422
3884
|
function registerIndexCommand(program) {
|
|
3423
3885
|
const index = program.command("index").description("Index management commands");
|
|
@@ -3433,18 +3895,16 @@ function registerIndexCommand(program) {
|
|
|
3433
3895
|
}
|
|
3434
3896
|
|
|
3435
3897
|
// src/commands/init.ts
|
|
3436
|
-
import
|
|
3437
|
-
import
|
|
3898
|
+
import fs10 from "fs";
|
|
3899
|
+
import path13 from "path";
|
|
3438
3900
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3439
|
-
import { createInterface } from "readline/promises";
|
|
3440
|
-
import { stdin as input, stdout as output } from "process";
|
|
3441
3901
|
import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
3442
3902
|
|
|
3443
3903
|
// src/hooks/pre-commit.ts
|
|
3444
|
-
import
|
|
3445
|
-
import
|
|
3904
|
+
import fs8 from "fs";
|
|
3905
|
+
import path11 from "path";
|
|
3446
3906
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3447
|
-
import { detect_cycle, parseTaskContent, parseTaskFile as
|
|
3907
|
+
import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural6 } from "@kitsy/coop-core";
|
|
3448
3908
|
var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
|
|
3449
3909
|
var HOOK_BLOCK_END = "# COOP_PRE_COMMIT_END";
|
|
3450
3910
|
function runGit(repoRoot, args, allowFailure = false) {
|
|
@@ -3473,28 +3933,28 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
|
|
|
3473
3933
|
const normalized = toPosixPath2(relativePath);
|
|
3474
3934
|
const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
|
|
3475
3935
|
if (projectMatch?.[1]) {
|
|
3476
|
-
return
|
|
3936
|
+
return path11.join(repoRoot, ".coop", "projects", projectMatch[1]);
|
|
3477
3937
|
}
|
|
3478
3938
|
if (normalized.startsWith(".coop/tasks/")) {
|
|
3479
|
-
return
|
|
3939
|
+
return path11.join(repoRoot, ".coop");
|
|
3480
3940
|
}
|
|
3481
3941
|
throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
|
|
3482
3942
|
}
|
|
3483
3943
|
function listTaskFilesForProject(projectRoot) {
|
|
3484
|
-
const tasksDir =
|
|
3485
|
-
if (!
|
|
3944
|
+
const tasksDir = path11.join(projectRoot, "tasks");
|
|
3945
|
+
if (!fs8.existsSync(tasksDir)) return [];
|
|
3486
3946
|
const out = [];
|
|
3487
3947
|
const stack = [tasksDir];
|
|
3488
3948
|
while (stack.length > 0) {
|
|
3489
3949
|
const current = stack.pop();
|
|
3490
|
-
const entries =
|
|
3950
|
+
const entries = fs8.readdirSync(current, { withFileTypes: true });
|
|
3491
3951
|
for (const entry of entries) {
|
|
3492
|
-
const fullPath =
|
|
3952
|
+
const fullPath = path11.join(current, entry.name);
|
|
3493
3953
|
if (entry.isDirectory()) {
|
|
3494
3954
|
stack.push(fullPath);
|
|
3495
3955
|
continue;
|
|
3496
3956
|
}
|
|
3497
|
-
if (entry.isFile() &&
|
|
3957
|
+
if (entry.isFile() && path11.extname(entry.name).toLowerCase() === ".md") {
|
|
3498
3958
|
out.push(fullPath);
|
|
3499
3959
|
}
|
|
3500
3960
|
}
|
|
@@ -3513,7 +3973,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3513
3973
|
const errors = [];
|
|
3514
3974
|
const staged = [];
|
|
3515
3975
|
for (const relativePath of relativePaths) {
|
|
3516
|
-
const absolutePath =
|
|
3976
|
+
const absolutePath = path11.join(repoRoot, ...relativePath.split("/"));
|
|
3517
3977
|
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
3518
3978
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
3519
3979
|
if (!stagedBlob) {
|
|
@@ -3528,7 +3988,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3528
3988
|
errors.push(`[COOP] ${message}`);
|
|
3529
3989
|
continue;
|
|
3530
3990
|
}
|
|
3531
|
-
const issues =
|
|
3991
|
+
const issues = validateStructural6(task, { filePath: absolutePath });
|
|
3532
3992
|
for (const issue of issues) {
|
|
3533
3993
|
errors.push(`[COOP] ${relativePath}: ${issue.message}`);
|
|
3534
3994
|
}
|
|
@@ -3577,13 +4037,13 @@ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
|
3577
4037
|
}
|
|
3578
4038
|
const tasks = [];
|
|
3579
4039
|
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
3580
|
-
const normalized = toPosixPath2(
|
|
4040
|
+
const normalized = toPosixPath2(path11.resolve(filePath));
|
|
3581
4041
|
const stagedTask = stagedByPath.get(normalized);
|
|
3582
4042
|
if (stagedTask) {
|
|
3583
4043
|
tasks.push(stagedTask);
|
|
3584
4044
|
continue;
|
|
3585
4045
|
}
|
|
3586
|
-
tasks.push(
|
|
4046
|
+
tasks.push(parseTaskFile8(filePath).task);
|
|
3587
4047
|
}
|
|
3588
4048
|
return tasks;
|
|
3589
4049
|
}
|
|
@@ -3607,7 +4067,7 @@ function runPreCommitChecks(repoRoot) {
|
|
|
3607
4067
|
const graph = buildGraphForCycleCheck(tasks);
|
|
3608
4068
|
const cycle = detect_cycle(graph);
|
|
3609
4069
|
if (cycle) {
|
|
3610
|
-
const projectLabel = toPosixPath2(
|
|
4070
|
+
const projectLabel = toPosixPath2(path11.relative(repoRoot, projectRoot));
|
|
3611
4071
|
errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
|
|
3612
4072
|
}
|
|
3613
4073
|
} catch (error) {
|
|
@@ -3639,9 +4099,9 @@ function hookScriptBlock() {
|
|
|
3639
4099
|
].join("\n");
|
|
3640
4100
|
}
|
|
3641
4101
|
function installPreCommitHook(repoRoot) {
|
|
3642
|
-
const hookPath =
|
|
3643
|
-
const hookDir =
|
|
3644
|
-
if (!
|
|
4102
|
+
const hookPath = path11.join(repoRoot, ".git", "hooks", "pre-commit");
|
|
4103
|
+
const hookDir = path11.dirname(hookPath);
|
|
4104
|
+
if (!fs8.existsSync(hookDir)) {
|
|
3645
4105
|
return {
|
|
3646
4106
|
installed: false,
|
|
3647
4107
|
hookPath,
|
|
@@ -3649,18 +4109,18 @@ function installPreCommitHook(repoRoot) {
|
|
|
3649
4109
|
};
|
|
3650
4110
|
}
|
|
3651
4111
|
const block = hookScriptBlock();
|
|
3652
|
-
if (!
|
|
4112
|
+
if (!fs8.existsSync(hookPath)) {
|
|
3653
4113
|
const content = ["#!/bin/sh", "", block].join("\n");
|
|
3654
|
-
|
|
4114
|
+
fs8.writeFileSync(hookPath, content, "utf8");
|
|
3655
4115
|
} else {
|
|
3656
|
-
const existing =
|
|
4116
|
+
const existing = fs8.readFileSync(hookPath, "utf8");
|
|
3657
4117
|
if (!existing.includes(HOOK_BLOCK_START)) {
|
|
3658
4118
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3659
|
-
|
|
4119
|
+
fs8.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3660
4120
|
}
|
|
3661
4121
|
}
|
|
3662
4122
|
try {
|
|
3663
|
-
|
|
4123
|
+
fs8.chmodSync(hookPath, 493);
|
|
3664
4124
|
} catch {
|
|
3665
4125
|
}
|
|
3666
4126
|
return {
|
|
@@ -3671,15 +4131,15 @@ function installPreCommitHook(repoRoot) {
|
|
|
3671
4131
|
}
|
|
3672
4132
|
|
|
3673
4133
|
// src/hooks/post-merge-validate.ts
|
|
3674
|
-
import
|
|
3675
|
-
import
|
|
4134
|
+
import fs9 from "fs";
|
|
4135
|
+
import path12 from "path";
|
|
3676
4136
|
import { list_projects } from "@kitsy/coop-core";
|
|
3677
|
-
import { load_graph as
|
|
4137
|
+
import { load_graph as load_graph5, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
3678
4138
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
3679
4139
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
3680
4140
|
function runPostMergeValidate(repoRoot) {
|
|
3681
|
-
const workspaceDir =
|
|
3682
|
-
if (!
|
|
4141
|
+
const workspaceDir = path12.join(repoRoot, ".coop");
|
|
4142
|
+
if (!fs9.existsSync(workspaceDir)) {
|
|
3683
4143
|
return {
|
|
3684
4144
|
ok: true,
|
|
3685
4145
|
warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
|
|
@@ -3695,7 +4155,7 @@ function runPostMergeValidate(repoRoot) {
|
|
|
3695
4155
|
}
|
|
3696
4156
|
const warnings = [];
|
|
3697
4157
|
for (const project of projects) {
|
|
3698
|
-
const graph =
|
|
4158
|
+
const graph = load_graph5(project.root);
|
|
3699
4159
|
const issues = validate_graph2(graph);
|
|
3700
4160
|
for (const issue of issues) {
|
|
3701
4161
|
warnings.push(`[COOP] post-merge warning [${project.id}] [${issue.invariant}] ${issue.message}`);
|
|
@@ -3731,9 +4191,9 @@ function postMergeHookBlock() {
|
|
|
3731
4191
|
].join("\n");
|
|
3732
4192
|
}
|
|
3733
4193
|
function installPostMergeHook(repoRoot) {
|
|
3734
|
-
const hookPath =
|
|
3735
|
-
const hookDir =
|
|
3736
|
-
if (!
|
|
4194
|
+
const hookPath = path12.join(repoRoot, ".git", "hooks", "post-merge");
|
|
4195
|
+
const hookDir = path12.dirname(hookPath);
|
|
4196
|
+
if (!fs9.existsSync(hookDir)) {
|
|
3737
4197
|
return {
|
|
3738
4198
|
installed: false,
|
|
3739
4199
|
hookPath,
|
|
@@ -3741,17 +4201,17 @@ function installPostMergeHook(repoRoot) {
|
|
|
3741
4201
|
};
|
|
3742
4202
|
}
|
|
3743
4203
|
const block = postMergeHookBlock();
|
|
3744
|
-
if (!
|
|
3745
|
-
|
|
4204
|
+
if (!fs9.existsSync(hookPath)) {
|
|
4205
|
+
fs9.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
|
|
3746
4206
|
} else {
|
|
3747
|
-
const existing =
|
|
4207
|
+
const existing = fs9.readFileSync(hookPath, "utf8");
|
|
3748
4208
|
if (!existing.includes(HOOK_BLOCK_START2)) {
|
|
3749
4209
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3750
|
-
|
|
4210
|
+
fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3751
4211
|
}
|
|
3752
4212
|
}
|
|
3753
4213
|
try {
|
|
3754
|
-
|
|
4214
|
+
fs9.chmodSync(hookPath, 493);
|
|
3755
4215
|
} catch {
|
|
3756
4216
|
}
|
|
3757
4217
|
return {
|
|
@@ -3762,6 +4222,28 @@ function installPostMergeHook(repoRoot) {
|
|
|
3762
4222
|
}
|
|
3763
4223
|
|
|
3764
4224
|
// src/commands/init.ts
|
|
4225
|
+
var NAMING_TEMPLATE_PRESETS = [
|
|
4226
|
+
{
|
|
4227
|
+
label: "<TYPE>-<TITLE16>-<SEQ>",
|
|
4228
|
+
value: "<TYPE>-<TITLE16>-<SEQ>",
|
|
4229
|
+
hint: "Balanced semantic default"
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
label: "<TYPE>-<TRACK>-<TITLE16>-<SEQ>",
|
|
4233
|
+
value: "<TYPE>-<TRACK>-<TITLE16>-<SEQ>",
|
|
4234
|
+
hint: "Include track in ids"
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
label: "<TYPE>-<USER>-<YYMMDD>-<RAND>",
|
|
4238
|
+
value: "<TYPE>-<USER>-<YYMMDD>-<RAND>",
|
|
4239
|
+
hint: "Actor/date/random oriented"
|
|
4240
|
+
},
|
|
4241
|
+
{
|
|
4242
|
+
label: "Custom template",
|
|
4243
|
+
value: "__custom__",
|
|
4244
|
+
hint: "Enter a custom naming template manually"
|
|
4245
|
+
}
|
|
4246
|
+
];
|
|
3765
4247
|
function normalizeProjectId(value) {
|
|
3766
4248
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
3767
4249
|
}
|
|
@@ -3917,40 +4399,40 @@ tmp/
|
|
|
3917
4399
|
*.tmp
|
|
3918
4400
|
`;
|
|
3919
4401
|
function ensureDir(dirPath) {
|
|
3920
|
-
|
|
4402
|
+
fs10.mkdirSync(dirPath, { recursive: true });
|
|
3921
4403
|
}
|
|
3922
4404
|
function writeIfMissing(filePath, content) {
|
|
3923
|
-
if (!
|
|
3924
|
-
|
|
4405
|
+
if (!fs10.existsSync(filePath)) {
|
|
4406
|
+
fs10.writeFileSync(filePath, content, "utf8");
|
|
3925
4407
|
}
|
|
3926
4408
|
}
|
|
3927
4409
|
function ensureGitignoreEntry(root, entry) {
|
|
3928
|
-
const gitignorePath =
|
|
3929
|
-
if (!
|
|
3930
|
-
|
|
4410
|
+
const gitignorePath = path13.join(root, ".gitignore");
|
|
4411
|
+
if (!fs10.existsSync(gitignorePath)) {
|
|
4412
|
+
fs10.writeFileSync(gitignorePath, `${entry}
|
|
3931
4413
|
`, "utf8");
|
|
3932
4414
|
return;
|
|
3933
4415
|
}
|
|
3934
|
-
const content =
|
|
4416
|
+
const content = fs10.readFileSync(gitignorePath, "utf8");
|
|
3935
4417
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3936
4418
|
if (!lines.includes(entry)) {
|
|
3937
4419
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3938
|
-
|
|
4420
|
+
fs10.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
3939
4421
|
`, "utf8");
|
|
3940
4422
|
}
|
|
3941
4423
|
}
|
|
3942
4424
|
function ensureGitattributesEntry(root, entry) {
|
|
3943
|
-
const attrsPath =
|
|
3944
|
-
if (!
|
|
3945
|
-
|
|
4425
|
+
const attrsPath = path13.join(root, ".gitattributes");
|
|
4426
|
+
if (!fs10.existsSync(attrsPath)) {
|
|
4427
|
+
fs10.writeFileSync(attrsPath, `${entry}
|
|
3946
4428
|
`, "utf8");
|
|
3947
4429
|
return;
|
|
3948
4430
|
}
|
|
3949
|
-
const content =
|
|
4431
|
+
const content = fs10.readFileSync(attrsPath, "utf8");
|
|
3950
4432
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3951
4433
|
if (!lines.includes(entry)) {
|
|
3952
4434
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3953
|
-
|
|
4435
|
+
fs10.writeFileSync(attrsPath, `${content}${suffix}${entry}
|
|
3954
4436
|
`, "utf8");
|
|
3955
4437
|
}
|
|
3956
4438
|
}
|
|
@@ -3982,36 +4464,27 @@ function installMergeDrivers(root) {
|
|
|
3982
4464
|
return "Updated .gitattributes but could not register merge drivers in git config.";
|
|
3983
4465
|
}
|
|
3984
4466
|
async function promptInitIdentity(root, options) {
|
|
3985
|
-
const rl = createInterface({ input, output });
|
|
3986
|
-
const ask2 = async (question, fallback) => {
|
|
3987
|
-
try {
|
|
3988
|
-
const answer = await rl.question(`${question} [${fallback}]: `);
|
|
3989
|
-
return answer.trim() || fallback;
|
|
3990
|
-
} catch {
|
|
3991
|
-
return fallback;
|
|
3992
|
-
}
|
|
3993
|
-
};
|
|
3994
4467
|
const defaultName = repoDisplayName(root);
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
projectId,
|
|
4009
|
-
projectAliases: parseAliases2(aliasesInput),
|
|
4010
|
-
namingTemplate
|
|
4011
|
-
};
|
|
4012
|
-
} finally {
|
|
4013
|
-
rl.close();
|
|
4468
|
+
const initialName = options.name?.trim() || defaultName;
|
|
4469
|
+
const projectName = options.name?.trim() || await ask("Project name", initialName);
|
|
4470
|
+
const defaultId = options.id?.trim() || normalizeProjectId(projectName) || repoIdentityId(root);
|
|
4471
|
+
const projectIdRaw = options.id?.trim() || await ask("Project id", defaultId);
|
|
4472
|
+
const projectId = normalizeProjectId(projectIdRaw);
|
|
4473
|
+
if (!projectId) {
|
|
4474
|
+
throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
|
|
4475
|
+
}
|
|
4476
|
+
const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask("Aliases (csv, optional)", "");
|
|
4477
|
+
let namingTemplate = options.naming?.trim();
|
|
4478
|
+
if (!namingTemplate) {
|
|
4479
|
+
const selected = await select("ID naming template", [...NAMING_TEMPLATE_PRESETS], 0);
|
|
4480
|
+
namingTemplate = selected === "__custom__" ? await ask("Custom ID naming template", DEFAULT_ID_NAMING_TEMPLATE) : selected;
|
|
4014
4481
|
}
|
|
4482
|
+
return {
|
|
4483
|
+
projectName,
|
|
4484
|
+
projectId,
|
|
4485
|
+
projectAliases: parseAliases2(aliasesInput),
|
|
4486
|
+
namingTemplate
|
|
4487
|
+
};
|
|
4015
4488
|
}
|
|
4016
4489
|
async function resolveInitIdentity(root, options) {
|
|
4017
4490
|
const defaultName = repoDisplayName(root);
|
|
@@ -4019,7 +4492,7 @@ async function resolveInitIdentity(root, options) {
|
|
|
4019
4492
|
const fallbackId = normalizeProjectId(options.id?.trim() || fallbackName) || repoIdentityId(root);
|
|
4020
4493
|
const fallbackAliases = parseAliases2(options.aliases);
|
|
4021
4494
|
const fallbackNaming = options.naming?.trim() || DEFAULT_ID_NAMING_TEMPLATE;
|
|
4022
|
-
const interactive = Boolean(options.interactive || !options.yes &&
|
|
4495
|
+
const interactive = Boolean(options.interactive || !options.yes && process.stdin.isTTY && process.stdout.isTTY);
|
|
4023
4496
|
if (interactive) {
|
|
4024
4497
|
return promptInitIdentity(root, options);
|
|
4025
4498
|
}
|
|
@@ -4036,7 +4509,7 @@ function registerInitCommand(program) {
|
|
|
4036
4509
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4037
4510
|
const identity = await resolveInitIdentity(root, options);
|
|
4038
4511
|
const projectId = identity.projectId;
|
|
4039
|
-
const projectRoot =
|
|
4512
|
+
const projectRoot = path13.join(workspaceDir, "projects", projectId);
|
|
4040
4513
|
const dirs = [
|
|
4041
4514
|
"ideas",
|
|
4042
4515
|
"tasks",
|
|
@@ -4052,23 +4525,23 @@ function registerInitCommand(program) {
|
|
|
4052
4525
|
"history/deliveries",
|
|
4053
4526
|
".index"
|
|
4054
4527
|
];
|
|
4055
|
-
ensureDir(
|
|
4528
|
+
ensureDir(path13.join(workspaceDir, "projects"));
|
|
4056
4529
|
for (const dir of dirs) {
|
|
4057
|
-
ensureDir(
|
|
4530
|
+
ensureDir(path13.join(projectRoot, dir));
|
|
4058
4531
|
}
|
|
4059
4532
|
writeIfMissing(
|
|
4060
|
-
|
|
4533
|
+
path13.join(projectRoot, "config.yml"),
|
|
4061
4534
|
buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
|
|
4062
4535
|
);
|
|
4063
|
-
if (!
|
|
4536
|
+
if (!fs10.existsSync(path13.join(projectRoot, "schema-version"))) {
|
|
4064
4537
|
write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
|
|
4065
4538
|
}
|
|
4066
|
-
writeIfMissing(
|
|
4067
|
-
writeIfMissing(
|
|
4068
|
-
writeIfMissing(
|
|
4069
|
-
writeIfMissing(
|
|
4070
|
-
writeIfMissing(
|
|
4071
|
-
writeIfMissing(
|
|
4539
|
+
writeIfMissing(path13.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
|
|
4540
|
+
writeIfMissing(path13.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
|
|
4541
|
+
writeIfMissing(path13.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
|
|
4542
|
+
writeIfMissing(path13.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
|
|
4543
|
+
writeIfMissing(path13.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
|
|
4544
|
+
writeIfMissing(path13.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
|
|
4072
4545
|
writeWorkspaceConfig(root, { version: 2, current_project: projectId });
|
|
4073
4546
|
ensureGitignoreEntry(root, ".coop/logs/");
|
|
4074
4547
|
ensureGitignoreEntry(root, ".coop/tmp/");
|
|
@@ -4078,30 +4551,32 @@ function registerInitCommand(program) {
|
|
|
4078
4551
|
const project = resolveProject(root, projectId);
|
|
4079
4552
|
console.log("Initialized COOP workspace.");
|
|
4080
4553
|
console.log(`- Root: ${root}`);
|
|
4081
|
-
console.log(`- Workspace: ${
|
|
4082
|
-
console.log(`- Project: ${project.id} (${
|
|
4554
|
+
console.log(`- Workspace: ${path13.relative(root, workspaceDir)}`);
|
|
4555
|
+
console.log(`- Project: ${project.id} (${path13.relative(root, project.root)})`);
|
|
4083
4556
|
console.log(`- Name: ${identity.projectName}`);
|
|
4084
4557
|
console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
4085
4558
|
console.log(`- ID naming: ${identity.namingTemplate}`);
|
|
4086
4559
|
console.log(`- ${preCommitHook.message}`);
|
|
4087
4560
|
if (preCommitHook.installed) {
|
|
4088
|
-
console.log(`- Hook: ${
|
|
4561
|
+
console.log(`- Hook: ${path13.relative(root, preCommitHook.hookPath)}`);
|
|
4089
4562
|
}
|
|
4090
4563
|
console.log(`- ${postMergeHook.message}`);
|
|
4091
4564
|
if (postMergeHook.installed) {
|
|
4092
|
-
console.log(`- Hook: ${
|
|
4565
|
+
console.log(`- Hook: ${path13.relative(root, postMergeHook.hookPath)}`);
|
|
4093
4566
|
}
|
|
4094
4567
|
console.log(`- ${mergeDrivers}`);
|
|
4095
4568
|
console.log("- Next steps:");
|
|
4096
4569
|
console.log(' 1. coop create idea "Describe the idea"');
|
|
4097
4570
|
console.log(' 2. coop create task "Describe the task"');
|
|
4098
4571
|
console.log(" 3. coop graph validate");
|
|
4572
|
+
console.log(` 4. coop help-ai --initial-prompt --strict --repo ${root.replace(/\\/g, "/")} --command coop.cmd`);
|
|
4573
|
+
console.log(" 5. after you create a delivery, add --delivery <id> to that help-ai prompt for agent handoff");
|
|
4099
4574
|
});
|
|
4100
4575
|
}
|
|
4101
4576
|
|
|
4102
4577
|
// src/commands/lifecycle.ts
|
|
4103
|
-
import
|
|
4104
|
-
import { parseTaskFile as
|
|
4578
|
+
import path14 from "path";
|
|
4579
|
+
import { parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
4105
4580
|
var lifecycleVerbs = [
|
|
4106
4581
|
{
|
|
4107
4582
|
name: "review",
|
|
@@ -4136,8 +4611,8 @@ var lifecycleVerbs = [
|
|
|
4136
4611
|
];
|
|
4137
4612
|
function currentTaskSelection(root, id) {
|
|
4138
4613
|
const reference = resolveReference(root, id, "task");
|
|
4139
|
-
const filePath =
|
|
4140
|
-
const parsed =
|
|
4614
|
+
const filePath = path14.join(root, ...reference.file.split("/"));
|
|
4615
|
+
const parsed = parseTaskFile9(filePath);
|
|
4141
4616
|
return formatSelectedTask(
|
|
4142
4617
|
{
|
|
4143
4618
|
task: parsed.task,
|
|
@@ -4161,16 +4636,16 @@ function registerLifecycleCommands(program) {
|
|
|
4161
4636
|
}
|
|
4162
4637
|
|
|
4163
4638
|
// src/commands/list.ts
|
|
4164
|
-
import
|
|
4165
|
-
import {
|
|
4639
|
+
import path15 from "path";
|
|
4640
|
+
import {
|
|
4641
|
+
effective_priority as effective_priority3,
|
|
4642
|
+
load_graph as load_graph6,
|
|
4643
|
+
parseDeliveryFile as parseDeliveryFile2,
|
|
4644
|
+
parseIdeaFile as parseIdeaFile4,
|
|
4645
|
+
parseTaskFile as parseTaskFile10,
|
|
4646
|
+
schedule_next as schedule_next3
|
|
4647
|
+
} from "@kitsy/coop-core";
|
|
4166
4648
|
import chalk2 from "chalk";
|
|
4167
|
-
|
|
4168
|
-
// src/utils/not-implemented.ts
|
|
4169
|
-
function printNotImplemented(command, phase) {
|
|
4170
|
-
console.log(`${command}: Not yet implemented - coming in Phase ${phase}.`);
|
|
4171
|
-
}
|
|
4172
|
-
|
|
4173
|
-
// src/commands/list.ts
|
|
4174
4649
|
function statusColor(status) {
|
|
4175
4650
|
switch (status) {
|
|
4176
4651
|
case "done":
|
|
@@ -4187,53 +4662,309 @@ function statusColor(status) {
|
|
|
4187
4662
|
return status;
|
|
4188
4663
|
}
|
|
4189
4664
|
}
|
|
4190
|
-
function
|
|
4191
|
-
return
|
|
4665
|
+
function parseColumns(input2) {
|
|
4666
|
+
return (input2 ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
4192
4667
|
}
|
|
4193
|
-
function
|
|
4668
|
+
function normalizeTaskColumns(value, ready) {
|
|
4669
|
+
if (!value?.trim()) {
|
|
4670
|
+
return ready ? ["id", "title", "priority", "status", "assignee", "score"] : ["id", "title", "priority", "status", "assignee"];
|
|
4671
|
+
}
|
|
4672
|
+
const raw = parseColumns(value);
|
|
4673
|
+
if (raw.length === 1 && raw[0] === "all") {
|
|
4674
|
+
return ["id", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
|
|
4675
|
+
}
|
|
4676
|
+
const normalized = raw.map((column) => {
|
|
4677
|
+
if (column === "p") return "priority";
|
|
4678
|
+
return column;
|
|
4679
|
+
});
|
|
4680
|
+
const valid = /* @__PURE__ */ new Set(["id", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
|
|
4681
|
+
for (const column of normalized) {
|
|
4682
|
+
if (!valid.has(column)) {
|
|
4683
|
+
throw new Error(`Invalid task column '${column}'. Expected id|title|status|priority|assignee|track|delivery|score|file|all.`);
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return normalized;
|
|
4687
|
+
}
|
|
4688
|
+
function normalizeIdeaColumns(value) {
|
|
4689
|
+
if (!value?.trim()) {
|
|
4690
|
+
return ["id", "title", "status"];
|
|
4691
|
+
}
|
|
4692
|
+
const raw = parseColumns(value);
|
|
4693
|
+
if (raw.length === 1 && raw[0] === "all") {
|
|
4694
|
+
return ["id", "title", "status", "file"];
|
|
4695
|
+
}
|
|
4696
|
+
const valid = /* @__PURE__ */ new Set(["id", "title", "status", "file"]);
|
|
4697
|
+
for (const column of raw) {
|
|
4698
|
+
if (!valid.has(column)) {
|
|
4699
|
+
throw new Error(`Invalid idea column '${column}'. Expected id|title|status|file|all.`);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return raw;
|
|
4703
|
+
}
|
|
4704
|
+
function normalizeTaskSort(value, fallback) {
|
|
4705
|
+
switch ((value ?? "").trim().toLowerCase()) {
|
|
4706
|
+
case "":
|
|
4707
|
+
return fallback;
|
|
4708
|
+
case "id":
|
|
4709
|
+
case "priority":
|
|
4710
|
+
case "status":
|
|
4711
|
+
case "title":
|
|
4712
|
+
case "updated":
|
|
4713
|
+
case "created":
|
|
4714
|
+
case "score":
|
|
4715
|
+
return value.trim().toLowerCase();
|
|
4716
|
+
default:
|
|
4717
|
+
throw new Error(`Invalid sort '${value}'. Expected id|priority|status|title|updated|created|score.`);
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
function normalizeIdeaSort(value, fallback) {
|
|
4721
|
+
switch ((value ?? "").trim().toLowerCase()) {
|
|
4722
|
+
case "":
|
|
4723
|
+
return fallback;
|
|
4724
|
+
case "id":
|
|
4725
|
+
case "status":
|
|
4726
|
+
case "title":
|
|
4727
|
+
case "updated":
|
|
4728
|
+
case "created":
|
|
4729
|
+
return value.trim().toLowerCase();
|
|
4730
|
+
default:
|
|
4731
|
+
throw new Error(`Invalid sort '${value}'. Expected id|status|title|updated|created.`);
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
function priorityRank(value) {
|
|
4735
|
+
switch (value) {
|
|
4736
|
+
case "p0":
|
|
4737
|
+
return 0;
|
|
4738
|
+
case "p1":
|
|
4739
|
+
return 1;
|
|
4740
|
+
case "p2":
|
|
4741
|
+
return 2;
|
|
4742
|
+
case "p3":
|
|
4743
|
+
return 3;
|
|
4744
|
+
default:
|
|
4745
|
+
return 4;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
function taskStatusRank(value) {
|
|
4749
|
+
switch (value) {
|
|
4750
|
+
case "in_progress":
|
|
4751
|
+
return 0;
|
|
4752
|
+
case "in_review":
|
|
4753
|
+
return 1;
|
|
4754
|
+
case "todo":
|
|
4755
|
+
return 2;
|
|
4756
|
+
case "blocked":
|
|
4757
|
+
return 3;
|
|
4758
|
+
case "done":
|
|
4759
|
+
return 4;
|
|
4760
|
+
case "canceled":
|
|
4761
|
+
return 5;
|
|
4762
|
+
default:
|
|
4763
|
+
return 6;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
function ideaStatusRank(value) {
|
|
4767
|
+
switch (value) {
|
|
4768
|
+
case "active":
|
|
4769
|
+
return 0;
|
|
4770
|
+
case "captured":
|
|
4771
|
+
return 1;
|
|
4772
|
+
case "validated":
|
|
4773
|
+
return 2;
|
|
4774
|
+
case "rejected":
|
|
4775
|
+
return 3;
|
|
4776
|
+
case "promoted":
|
|
4777
|
+
return 4;
|
|
4778
|
+
default:
|
|
4779
|
+
return 5;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
function compareDatesDesc(a, b) {
|
|
4783
|
+
return (b ?? "").localeCompare(a ?? "");
|
|
4784
|
+
}
|
|
4785
|
+
function compareTaskScoreLike(a, b, readyOrder, track) {
|
|
4786
|
+
const aReady = readyOrder.get(a.id);
|
|
4787
|
+
const bReady = readyOrder.get(b.id);
|
|
4788
|
+
if (aReady !== void 0 || bReady !== void 0) {
|
|
4789
|
+
if (aReady === void 0) return 1;
|
|
4790
|
+
if (bReady === void 0) return -1;
|
|
4791
|
+
return aReady - bReady;
|
|
4792
|
+
}
|
|
4793
|
+
const status = taskStatusRank(a.status) - taskStatusRank(b.status);
|
|
4794
|
+
if (status !== 0) return status;
|
|
4795
|
+
const priority = priorityRank(effective_priority3(b.task, track)) - priorityRank(effective_priority3(a.task, track));
|
|
4796
|
+
if (priority !== 0) return priority;
|
|
4797
|
+
const updated = compareDatesDesc(a.task.updated, b.task.updated);
|
|
4798
|
+
if (updated !== 0) return updated;
|
|
4799
|
+
return a.id.localeCompare(b.id);
|
|
4800
|
+
}
|
|
4801
|
+
function sortTaskRows(rows, sortMode, readyOrder, track) {
|
|
4802
|
+
return [...rows].sort((a, b) => {
|
|
4803
|
+
switch (sortMode) {
|
|
4804
|
+
case "score":
|
|
4805
|
+
return compareTaskScoreLike(a, b, readyOrder, track);
|
|
4806
|
+
case "priority": {
|
|
4807
|
+
const priority = priorityRank(a.priority) - priorityRank(b.priority);
|
|
4808
|
+
if (priority !== 0) return priority;
|
|
4809
|
+
return a.id.localeCompare(b.id);
|
|
4810
|
+
}
|
|
4811
|
+
case "status": {
|
|
4812
|
+
const status = taskStatusRank(a.status) - taskStatusRank(b.status);
|
|
4813
|
+
if (status !== 0) return status;
|
|
4814
|
+
return a.id.localeCompare(b.id);
|
|
4815
|
+
}
|
|
4816
|
+
case "title":
|
|
4817
|
+
return a.title.localeCompare(b.title);
|
|
4818
|
+
case "updated": {
|
|
4819
|
+
const updated = compareDatesDesc(a.task.updated, b.task.updated);
|
|
4820
|
+
if (updated !== 0) return updated;
|
|
4821
|
+
return a.id.localeCompare(b.id);
|
|
4822
|
+
}
|
|
4823
|
+
case "created": {
|
|
4824
|
+
const created = compareDatesDesc(a.task.created, b.task.created);
|
|
4825
|
+
if (created !== 0) return created;
|
|
4826
|
+
return a.id.localeCompare(b.id);
|
|
4827
|
+
}
|
|
4828
|
+
case "id":
|
|
4829
|
+
default:
|
|
4830
|
+
return a.id.localeCompare(b.id);
|
|
4831
|
+
}
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
function taskColumnHeader(column) {
|
|
4835
|
+
switch (column) {
|
|
4836
|
+
case "priority":
|
|
4837
|
+
return "P";
|
|
4838
|
+
case "assignee":
|
|
4839
|
+
return "Assignee";
|
|
4840
|
+
case "track":
|
|
4841
|
+
return "Track";
|
|
4842
|
+
case "delivery":
|
|
4843
|
+
return "Delivery";
|
|
4844
|
+
case "score":
|
|
4845
|
+
return "Score";
|
|
4846
|
+
case "file":
|
|
4847
|
+
return "File";
|
|
4848
|
+
case "status":
|
|
4849
|
+
return "Status";
|
|
4850
|
+
case "title":
|
|
4851
|
+
return "Title";
|
|
4852
|
+
case "id":
|
|
4853
|
+
default:
|
|
4854
|
+
return "ID";
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
function ideaColumnHeader(column) {
|
|
4858
|
+
switch (column) {
|
|
4859
|
+
case "file":
|
|
4860
|
+
return "File";
|
|
4861
|
+
case "status":
|
|
4862
|
+
return "Status";
|
|
4863
|
+
case "title":
|
|
4864
|
+
return "Title";
|
|
4865
|
+
case "id":
|
|
4866
|
+
default:
|
|
4867
|
+
return "ID";
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
function loadTasks2(root) {
|
|
4194
4871
|
return listTaskFiles(root).map((filePath) => ({
|
|
4195
|
-
task:
|
|
4872
|
+
task: parseTaskFile10(filePath).task,
|
|
4196
4873
|
filePath
|
|
4197
4874
|
}));
|
|
4198
4875
|
}
|
|
4199
4876
|
function loadIdeas(root) {
|
|
4200
4877
|
return listIdeaFiles(root).map((filePath) => ({
|
|
4201
|
-
idea:
|
|
4878
|
+
idea: parseIdeaFile4(filePath).idea,
|
|
4202
4879
|
filePath
|
|
4203
4880
|
}));
|
|
4204
4881
|
}
|
|
4205
4882
|
function listTasks(options) {
|
|
4206
4883
|
const root = resolveRepoRoot();
|
|
4207
4884
|
ensureCoopInitialized(root);
|
|
4208
|
-
const
|
|
4885
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
4886
|
+
const graph = load_graph6(coopDir(root));
|
|
4887
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track);
|
|
4888
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
|
|
4889
|
+
const resolvedVersion = resolveContextValueWithSource(options.version, context.version);
|
|
4890
|
+
const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
|
|
4891
|
+
const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
|
|
4892
|
+
const defaultSort = options.ready || resolvedTrack.value || resolvedDelivery.value ? "score" : "id";
|
|
4893
|
+
const sortMode = normalizeTaskSort(options.sort, defaultSort);
|
|
4894
|
+
const columns = normalizeTaskColumns(options.columns, Boolean(options.ready));
|
|
4895
|
+
if (isVerboseRequested()) {
|
|
4896
|
+
for (const line of formatResolvedContextMessage({
|
|
4897
|
+
track: resolvedTrack,
|
|
4898
|
+
delivery: resolvedDelivery,
|
|
4899
|
+
version: resolvedVersion
|
|
4900
|
+
})) {
|
|
4901
|
+
console.log(line);
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
const scoredEntries = sortMode === "score" || options.ready ? schedule_next3(graph, {
|
|
4905
|
+
track: resolvedTrack.value,
|
|
4906
|
+
delivery: resolvedDelivery.value
|
|
4907
|
+
}) : [];
|
|
4908
|
+
const readyIds = options.ready ? new Set(scoredEntries.map((entry) => entry.task.id)) : null;
|
|
4909
|
+
const readyOrder = new Map(scoredEntries.map((entry, index) => [entry.task.id, index]));
|
|
4910
|
+
const scoreMap = new Map(scoredEntries.map((entry) => [entry.task.id, entry.score]));
|
|
4911
|
+
const rows = loadTasks2(root).filter(({ task }) => {
|
|
4912
|
+
if (readyIds && !readyIds.has(task.id)) return false;
|
|
4209
4913
|
if (options.status && task.status !== options.status) return false;
|
|
4210
|
-
if (
|
|
4211
|
-
|
|
4914
|
+
if (resolvedTrack.value && task.track !== resolvedTrack.value && !(task.delivery_tracks ?? []).includes(resolvedTrack.value)) {
|
|
4915
|
+
return false;
|
|
4916
|
+
}
|
|
4917
|
+
if (resolvedDelivery.value && task.delivery !== resolvedDelivery.value && !deliveryScope?.has(task.id)) return false;
|
|
4918
|
+
if (options.priority && taskEffectivePriority(task, resolvedTrack.value) !== options.priority) return false;
|
|
4919
|
+
if (assignee && (task.assignee ?? "") !== assignee) return false;
|
|
4920
|
+
if (resolvedVersion.value && !(task.fix_versions ?? []).includes(resolvedVersion.value) && !(task.released_in ?? []).includes(resolvedVersion.value)) {
|
|
4921
|
+
return false;
|
|
4922
|
+
}
|
|
4212
4923
|
return true;
|
|
4213
4924
|
}).map(({ task, filePath }) => ({
|
|
4925
|
+
task,
|
|
4214
4926
|
id: task.id,
|
|
4215
4927
|
title: task.title,
|
|
4216
4928
|
status: task.status,
|
|
4217
|
-
priority: task.
|
|
4929
|
+
priority: taskEffectivePriority(task, resolvedTrack.value),
|
|
4218
4930
|
track: task.track ?? "-",
|
|
4931
|
+
assignee: task.assignee ?? "-",
|
|
4932
|
+
delivery: task.delivery ?? "-",
|
|
4219
4933
|
filePath
|
|
4220
4934
|
}));
|
|
4221
|
-
const sorted =
|
|
4935
|
+
const sorted = sortTaskRows(rows, sortMode, readyOrder, resolvedTrack.value);
|
|
4222
4936
|
if (sorted.length === 0) {
|
|
4223
4937
|
console.log("No tasks found.");
|
|
4224
4938
|
return;
|
|
4225
4939
|
}
|
|
4226
4940
|
console.log(
|
|
4227
4941
|
formatTable(
|
|
4228
|
-
|
|
4229
|
-
sorted.map(
|
|
4230
|
-
entry.
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4942
|
+
columns.map(taskColumnHeader),
|
|
4943
|
+
sorted.map(
|
|
4944
|
+
(entry) => columns.map((column) => {
|
|
4945
|
+
switch (column) {
|
|
4946
|
+
case "title":
|
|
4947
|
+
return entry.title;
|
|
4948
|
+
case "status":
|
|
4949
|
+
return statusColor(entry.status);
|
|
4950
|
+
case "priority":
|
|
4951
|
+
return entry.priority;
|
|
4952
|
+
case "assignee":
|
|
4953
|
+
return entry.assignee;
|
|
4954
|
+
case "track":
|
|
4955
|
+
return entry.track;
|
|
4956
|
+
case "delivery":
|
|
4957
|
+
return entry.delivery;
|
|
4958
|
+
case "score":
|
|
4959
|
+
return scoreMap.has(entry.id) ? scoreMap.get(entry.id).toFixed(1) : "-";
|
|
4960
|
+
case "file":
|
|
4961
|
+
return path15.relative(root, entry.filePath);
|
|
4962
|
+
case "id":
|
|
4963
|
+
default:
|
|
4964
|
+
return entry.id;
|
|
4965
|
+
}
|
|
4966
|
+
})
|
|
4967
|
+
)
|
|
4237
4968
|
)
|
|
4238
4969
|
);
|
|
4239
4970
|
console.log(`
|
|
@@ -4242,6 +4973,8 @@ Total tasks: ${sorted.length}`);
|
|
|
4242
4973
|
function listIdeas(options) {
|
|
4243
4974
|
const root = resolveRepoRoot();
|
|
4244
4975
|
ensureCoopInitialized(root);
|
|
4976
|
+
const sortMode = normalizeIdeaSort(options.sort, "id");
|
|
4977
|
+
const columns = normalizeIdeaColumns(options.columns);
|
|
4245
4978
|
const rows = loadIdeas(root).filter(({ idea }) => {
|
|
4246
4979
|
if (options.status && idea.status !== options.status) return false;
|
|
4247
4980
|
return true;
|
|
@@ -4253,64 +4986,115 @@ function listIdeas(options) {
|
|
|
4253
4986
|
track: "-",
|
|
4254
4987
|
filePath
|
|
4255
4988
|
}));
|
|
4256
|
-
const sorted =
|
|
4989
|
+
const sorted = [...rows].sort((a, b) => {
|
|
4990
|
+
switch (sortMode) {
|
|
4991
|
+
case "status": {
|
|
4992
|
+
const status = ideaStatusRank(a.status) - ideaStatusRank(b.status);
|
|
4993
|
+
if (status !== 0) return status;
|
|
4994
|
+
return a.id.localeCompare(b.id);
|
|
4995
|
+
}
|
|
4996
|
+
case "title":
|
|
4997
|
+
return a.title.localeCompare(b.title);
|
|
4998
|
+
case "updated":
|
|
4999
|
+
case "created":
|
|
5000
|
+
return a.id.localeCompare(b.id);
|
|
5001
|
+
case "id":
|
|
5002
|
+
default:
|
|
5003
|
+
return a.id.localeCompare(b.id);
|
|
5004
|
+
}
|
|
5005
|
+
});
|
|
4257
5006
|
if (sorted.length === 0) {
|
|
4258
5007
|
console.log("No ideas found.");
|
|
4259
5008
|
return;
|
|
4260
5009
|
}
|
|
4261
5010
|
console.log(
|
|
4262
5011
|
formatTable(
|
|
4263
|
-
|
|
4264
|
-
sorted.map(
|
|
4265
|
-
entry.
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
5012
|
+
columns.map(ideaColumnHeader),
|
|
5013
|
+
sorted.map(
|
|
5014
|
+
(entry) => columns.map((column) => {
|
|
5015
|
+
switch (column) {
|
|
5016
|
+
case "title":
|
|
5017
|
+
return entry.title;
|
|
5018
|
+
case "status":
|
|
5019
|
+
return statusColor(entry.status);
|
|
5020
|
+
case "file":
|
|
5021
|
+
return path15.relative(root, entry.filePath);
|
|
5022
|
+
case "id":
|
|
5023
|
+
default:
|
|
5024
|
+
return entry.id;
|
|
5025
|
+
}
|
|
5026
|
+
})
|
|
5027
|
+
)
|
|
4272
5028
|
)
|
|
4273
5029
|
);
|
|
4274
5030
|
console.log(`
|
|
4275
5031
|
Total ideas: ${sorted.length}`);
|
|
4276
5032
|
}
|
|
5033
|
+
function listDeliveries() {
|
|
5034
|
+
const root = resolveRepoRoot();
|
|
5035
|
+
const rows = listDeliveryFiles(root).map((filePath) => ({ delivery: parseDeliveryFile2(filePath).delivery, filePath })).map(({ delivery, filePath }) => [
|
|
5036
|
+
delivery.id,
|
|
5037
|
+
delivery.name,
|
|
5038
|
+
statusColor(delivery.status),
|
|
5039
|
+
delivery.target_date ?? "-",
|
|
5040
|
+
path15.relative(root, filePath)
|
|
5041
|
+
]);
|
|
5042
|
+
if (rows.length === 0) {
|
|
5043
|
+
console.log("No deliveries found.");
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
|
|
5047
|
+
}
|
|
4277
5048
|
function registerListCommand(program) {
|
|
4278
5049
|
const list = program.command("list").description("List COOP entities");
|
|
4279
|
-
list.command("tasks").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by track").option("--priority <priority>", "Filter by priority").action((options) => {
|
|
5050
|
+
list.command("tasks").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
|
|
4280
5051
|
listTasks(options);
|
|
4281
5052
|
});
|
|
4282
|
-
list.command("ideas").description("List ideas").option("--status <status>", "Filter by status").action((options) => {
|
|
5053
|
+
list.command("ideas").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,title,status,file or all").action((options) => {
|
|
4283
5054
|
listIdeas(options);
|
|
4284
5055
|
});
|
|
4285
5056
|
list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
|
|
4286
5057
|
listAliasRows(pattern);
|
|
4287
5058
|
});
|
|
4288
|
-
list.command("deliveries").description("List deliveries
|
|
4289
|
-
|
|
5059
|
+
list.command("deliveries").description("List deliveries").action(() => {
|
|
5060
|
+
listDeliveries();
|
|
4290
5061
|
});
|
|
4291
5062
|
}
|
|
4292
5063
|
|
|
4293
5064
|
// src/utils/logger.ts
|
|
4294
|
-
import
|
|
4295
|
-
import
|
|
4296
|
-
function
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
5065
|
+
import fs11 from "fs";
|
|
5066
|
+
import path16 from "path";
|
|
5067
|
+
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5068
|
+
let current = path16.resolve(start);
|
|
5069
|
+
while (true) {
|
|
5070
|
+
const gitDir = path16.join(current, ".git");
|
|
5071
|
+
const coopDir2 = coopWorkspaceDir(current);
|
|
5072
|
+
const workspaceConfig = path16.join(coopDir2, "config.yml");
|
|
5073
|
+
const projectsDir = path16.join(coopDir2, "projects");
|
|
5074
|
+
if (fs11.existsSync(gitDir)) {
|
|
5075
|
+
return current;
|
|
5076
|
+
}
|
|
5077
|
+
if (fs11.existsSync(workspaceConfig) || fs11.existsSync(projectsDir)) {
|
|
5078
|
+
return current;
|
|
5079
|
+
}
|
|
5080
|
+
const parent = path16.dirname(current);
|
|
5081
|
+
if (parent === current) {
|
|
5082
|
+
return null;
|
|
5083
|
+
}
|
|
5084
|
+
current = parent;
|
|
4301
5085
|
}
|
|
4302
5086
|
}
|
|
4303
5087
|
function resolveCliLogFile(start = process.cwd()) {
|
|
4304
|
-
const root =
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
return
|
|
5088
|
+
const root = resolveWorkspaceRoot(start);
|
|
5089
|
+
if (root) {
|
|
5090
|
+
const workspace = coopWorkspaceDir(root);
|
|
5091
|
+
return path16.join(workspace, "logs", "cli.log");
|
|
4308
5092
|
}
|
|
4309
|
-
return
|
|
5093
|
+
return path16.join(resolveCoopHome(), "logs", "cli.log");
|
|
4310
5094
|
}
|
|
4311
5095
|
function appendLogEntry(entry, logFile) {
|
|
4312
|
-
|
|
4313
|
-
|
|
5096
|
+
fs11.mkdirSync(path16.dirname(logFile), { recursive: true });
|
|
5097
|
+
fs11.appendFileSync(logFile, `${JSON.stringify(entry)}
|
|
4314
5098
|
`, "utf8");
|
|
4315
5099
|
}
|
|
4316
5100
|
function logCliError(error, start = process.cwd()) {
|
|
@@ -4349,8 +5133,8 @@ function parseLogLine(line) {
|
|
|
4349
5133
|
}
|
|
4350
5134
|
function readLastCliLog(start = process.cwd()) {
|
|
4351
5135
|
const logFile = resolveCliLogFile(start);
|
|
4352
|
-
if (!
|
|
4353
|
-
const content =
|
|
5136
|
+
if (!fs11.existsSync(logFile)) return null;
|
|
5137
|
+
const content = fs11.readFileSync(logFile, "utf8");
|
|
4354
5138
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4355
5139
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
4356
5140
|
const entry = parseLogLine(lines[i] ?? "");
|
|
@@ -4384,11 +5168,39 @@ function registerLogCommand(program) {
|
|
|
4384
5168
|
});
|
|
4385
5169
|
}
|
|
4386
5170
|
|
|
5171
|
+
// src/commands/log-time.ts
|
|
5172
|
+
function registerLogTimeCommand(program) {
|
|
5173
|
+
program.command("log-time").description("Append planned or worked time to a task").argument("<id-or-type>", "Task id/alias, or the literal `task` followed by an id").argument("[id]", "Task id when an explicit entity type is provided").requiredOption("--hours <n>", "Hours to log").requiredOption("--kind <kind>", "planned|worked").option("--note <text>", "Optional note").option("--actor <id>", "Actor for the log entry").action((first, second, options) => {
|
|
5174
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
5175
|
+
if (entity !== "task") {
|
|
5176
|
+
throw new Error("Only task time logging is supported.");
|
|
5177
|
+
}
|
|
5178
|
+
const hours = Number(options.hours);
|
|
5179
|
+
if (!Number.isFinite(hours) || hours < 0) {
|
|
5180
|
+
throw new Error(`Invalid --hours value '${options.hours}'. Expected a non-negative number.`);
|
|
5181
|
+
}
|
|
5182
|
+
if (options.kind !== "planned" && options.kind !== "worked") {
|
|
5183
|
+
throw new Error(`Invalid --kind value '${options.kind}'. Expected planned|worked.`);
|
|
5184
|
+
}
|
|
5185
|
+
const root = resolveRepoRoot();
|
|
5186
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
5187
|
+
const task = appendTaskTimeLog(
|
|
5188
|
+
parsed.task,
|
|
5189
|
+
options.actor?.trim() || defaultCoopAuthor(root),
|
|
5190
|
+
options.kind,
|
|
5191
|
+
hours,
|
|
5192
|
+
options.note?.trim() || void 0
|
|
5193
|
+
);
|
|
5194
|
+
writeTaskEntry(filePath, parsed, task);
|
|
5195
|
+
console.log(`Logged ${hours}h ${options.kind} on ${task.id}`);
|
|
5196
|
+
});
|
|
5197
|
+
}
|
|
5198
|
+
|
|
4387
5199
|
// src/commands/migrate.ts
|
|
4388
|
-
import
|
|
4389
|
-
import
|
|
4390
|
-
import { createInterface
|
|
4391
|
-
import { stdin as
|
|
5200
|
+
import fs12 from "fs";
|
|
5201
|
+
import path17 from "path";
|
|
5202
|
+
import { createInterface } from "readline/promises";
|
|
5203
|
+
import { stdin as input, stdout as output } from "process";
|
|
4392
5204
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
|
|
4393
5205
|
var COOP_IGNORE_TEMPLATE2 = `.index/
|
|
4394
5206
|
logs/
|
|
@@ -4405,22 +5217,22 @@ function parseTargetVersion(raw) {
|
|
|
4405
5217
|
return parsed;
|
|
4406
5218
|
}
|
|
4407
5219
|
function writeIfMissing2(filePath, content) {
|
|
4408
|
-
if (!
|
|
4409
|
-
|
|
5220
|
+
if (!fs12.existsSync(filePath)) {
|
|
5221
|
+
fs12.writeFileSync(filePath, content, "utf8");
|
|
4410
5222
|
}
|
|
4411
5223
|
}
|
|
4412
5224
|
function ensureGitignoreEntry2(root, entry) {
|
|
4413
|
-
const gitignorePath =
|
|
4414
|
-
if (!
|
|
4415
|
-
|
|
5225
|
+
const gitignorePath = path17.join(root, ".gitignore");
|
|
5226
|
+
if (!fs12.existsSync(gitignorePath)) {
|
|
5227
|
+
fs12.writeFileSync(gitignorePath, `${entry}
|
|
4416
5228
|
`, "utf8");
|
|
4417
5229
|
return;
|
|
4418
5230
|
}
|
|
4419
|
-
const content =
|
|
5231
|
+
const content = fs12.readFileSync(gitignorePath, "utf8");
|
|
4420
5232
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
4421
5233
|
if (!lines.includes(entry)) {
|
|
4422
5234
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
4423
|
-
|
|
5235
|
+
fs12.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
4424
5236
|
`, "utf8");
|
|
4425
5237
|
}
|
|
4426
5238
|
}
|
|
@@ -4445,7 +5257,7 @@ function legacyWorkspaceProjectEntries(root) {
|
|
|
4445
5257
|
"backlog",
|
|
4446
5258
|
"plans",
|
|
4447
5259
|
"releases"
|
|
4448
|
-
].filter((entry) =>
|
|
5260
|
+
].filter((entry) => fs12.existsSync(path17.join(workspaceDir, entry)));
|
|
4449
5261
|
}
|
|
4450
5262
|
function normalizeProjectId2(value) {
|
|
4451
5263
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
@@ -4461,7 +5273,7 @@ function parseAliases3(value) {
|
|
|
4461
5273
|
);
|
|
4462
5274
|
}
|
|
4463
5275
|
async function promptProjectIdentity(defaults, options) {
|
|
4464
|
-
const rl =
|
|
5276
|
+
const rl = createInterface({ input, output });
|
|
4465
5277
|
const ask2 = async (question, fallback) => {
|
|
4466
5278
|
try {
|
|
4467
5279
|
const answer = await rl.question(`${question} [${fallback}]: `);
|
|
@@ -4490,11 +5302,11 @@ async function promptProjectIdentity(defaults, options) {
|
|
|
4490
5302
|
async function resolveMigrationIdentity(root, options) {
|
|
4491
5303
|
const existing = readCoopConfig(root);
|
|
4492
5304
|
const defaults = {
|
|
4493
|
-
projectName: existing.projectName ||
|
|
5305
|
+
projectName: existing.projectName || path17.basename(root),
|
|
4494
5306
|
projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
|
|
4495
5307
|
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
|
|
4496
5308
|
};
|
|
4497
|
-
const interactive = Boolean(options.interactive || !options.yes &&
|
|
5309
|
+
const interactive = Boolean(options.interactive || !options.yes && input.isTTY && output.isTTY);
|
|
4498
5310
|
if (interactive) {
|
|
4499
5311
|
return promptProjectIdentity(defaults, options);
|
|
4500
5312
|
}
|
|
@@ -4510,22 +5322,22 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4510
5322
|
throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
|
|
4511
5323
|
}
|
|
4512
5324
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4513
|
-
if (!
|
|
5325
|
+
if (!fs12.existsSync(workspaceDir)) {
|
|
4514
5326
|
throw new Error("Missing .coop directory. Run 'coop init' first.");
|
|
4515
5327
|
}
|
|
4516
|
-
const projectsDir =
|
|
5328
|
+
const projectsDir = path17.join(workspaceDir, "projects");
|
|
4517
5329
|
const legacyEntries = legacyWorkspaceProjectEntries(root);
|
|
4518
|
-
if (legacyEntries.length === 0 &&
|
|
5330
|
+
if (legacyEntries.length === 0 && fs12.existsSync(projectsDir)) {
|
|
4519
5331
|
console.log("[COOP] workspace layout already uses v2.");
|
|
4520
5332
|
return;
|
|
4521
5333
|
}
|
|
4522
5334
|
const identity = await resolveMigrationIdentity(root, options);
|
|
4523
5335
|
const projectId = identity.projectId;
|
|
4524
|
-
const projectRoot =
|
|
4525
|
-
if (
|
|
4526
|
-
throw new Error(`Project destination '${
|
|
5336
|
+
const projectRoot = path17.join(projectsDir, projectId);
|
|
5337
|
+
if (fs12.existsSync(projectRoot) && !options.force) {
|
|
5338
|
+
throw new Error(`Project destination '${path17.relative(root, projectRoot)}' already exists. Re-run with --force.`);
|
|
4527
5339
|
}
|
|
4528
|
-
const changed = legacyEntries.map((entry) => `${
|
|
5340
|
+
const changed = legacyEntries.map((entry) => `${path17.join(".coop", entry)} -> ${path17.join(".coop", "projects", projectId, entry)}`);
|
|
4529
5341
|
changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
|
|
4530
5342
|
console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
|
|
4531
5343
|
console.log(`- from: v1 flat layout`);
|
|
@@ -4541,21 +5353,21 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4541
5353
|
console.log("- no files were modified.");
|
|
4542
5354
|
return;
|
|
4543
5355
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
5356
|
+
fs12.mkdirSync(projectsDir, { recursive: true });
|
|
5357
|
+
fs12.mkdirSync(projectRoot, { recursive: true });
|
|
4546
5358
|
for (const entry of legacyEntries) {
|
|
4547
|
-
const source =
|
|
4548
|
-
const destination =
|
|
4549
|
-
if (
|
|
5359
|
+
const source = path17.join(workspaceDir, entry);
|
|
5360
|
+
const destination = path17.join(projectRoot, entry);
|
|
5361
|
+
if (fs12.existsSync(destination)) {
|
|
4550
5362
|
if (!options.force) {
|
|
4551
|
-
throw new Error(`Destination '${
|
|
5363
|
+
throw new Error(`Destination '${path17.relative(root, destination)}' already exists.`);
|
|
4552
5364
|
}
|
|
4553
|
-
|
|
5365
|
+
fs12.rmSync(destination, { recursive: true, force: true });
|
|
4554
5366
|
}
|
|
4555
|
-
|
|
5367
|
+
fs12.renameSync(source, destination);
|
|
4556
5368
|
}
|
|
4557
|
-
const movedConfigPath =
|
|
4558
|
-
if (
|
|
5369
|
+
const movedConfigPath = path17.join(projectRoot, "config.yml");
|
|
5370
|
+
if (fs12.existsSync(movedConfigPath)) {
|
|
4559
5371
|
const movedConfig = parseYamlFile2(movedConfigPath);
|
|
4560
5372
|
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
4561
5373
|
nextProject.name = identity.projectName;
|
|
@@ -4572,13 +5384,13 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4572
5384
|
}
|
|
4573
5385
|
const workspace = readWorkspaceConfig(root);
|
|
4574
5386
|
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
|
|
4575
|
-
writeIfMissing2(
|
|
4576
|
-
writeIfMissing2(
|
|
5387
|
+
writeIfMissing2(path17.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
|
|
5388
|
+
writeIfMissing2(path17.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
|
|
4577
5389
|
ensureGitignoreEntry2(root, ".coop/logs/");
|
|
4578
5390
|
ensureGitignoreEntry2(root, ".coop/tmp/");
|
|
4579
5391
|
const manager = new IndexManager3(projectRoot);
|
|
4580
5392
|
manager.build_full_index();
|
|
4581
|
-
console.log(`[COOP] migrated workspace to v2 at ${
|
|
5393
|
+
console.log(`[COOP] migrated workspace to v2 at ${path17.relative(root, projectRoot)}`);
|
|
4582
5394
|
}
|
|
4583
5395
|
function registerMigrateCommand(program) {
|
|
4584
5396
|
const migrate = program.command("migrate").description("Migrate COOP data and workspace layouts").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target schema version", String(CURRENT_SCHEMA_VERSION2)).action((options) => {
|
|
@@ -4597,7 +5409,7 @@ function registerMigrateCommand(program) {
|
|
|
4597
5409
|
if (report.changed_files.length > 0) {
|
|
4598
5410
|
console.log("- changed files:");
|
|
4599
5411
|
for (const filePath of report.changed_files) {
|
|
4600
|
-
console.log(` - ${
|
|
5412
|
+
console.log(` - ${path17.relative(root, filePath)}`);
|
|
4601
5413
|
}
|
|
4602
5414
|
}
|
|
4603
5415
|
if (report.dry_run) {
|
|
@@ -4691,7 +5503,7 @@ import chalk3 from "chalk";
|
|
|
4691
5503
|
import {
|
|
4692
5504
|
analyze_feasibility,
|
|
4693
5505
|
analyze_what_if,
|
|
4694
|
-
load_graph as
|
|
5506
|
+
load_graph as load_graph7,
|
|
4695
5507
|
monte_carlo_forecast,
|
|
4696
5508
|
TaskPriority as TaskPriority2
|
|
4697
5509
|
} from "@kitsy/coop-core";
|
|
@@ -4755,8 +5567,19 @@ function collectWhatIfModifications(options) {
|
|
|
4755
5567
|
}
|
|
4756
5568
|
async function runPlanDelivery(deliveryName, options) {
|
|
4757
5569
|
const root = resolveRepoRoot();
|
|
4758
|
-
const
|
|
4759
|
-
const
|
|
5570
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5571
|
+
const graph = load_graph7(coopDir(root));
|
|
5572
|
+
const resolvedDelivery = deliveryName?.trim() ? { value: deliveryName.trim(), source: "arg" } : resolveContextValueWithSource(void 0, context.delivery, sharedDefault(root, "delivery"));
|
|
5573
|
+
if (isVerboseRequested()) {
|
|
5574
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
5575
|
+
console.log(line);
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
const deliveryRef = resolvedDelivery.value;
|
|
5579
|
+
if (!deliveryRef) {
|
|
5580
|
+
throw new Error("delivery is not set; pass a delivery id or set a default working delivery with `coop use delivery <id>`.");
|
|
5581
|
+
}
|
|
5582
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
4760
5583
|
const modifications = collectWhatIfModifications(options);
|
|
4761
5584
|
if (modifications.length > 0) {
|
|
4762
5585
|
const comparison = analyze_what_if(
|
|
@@ -4871,7 +5694,7 @@ async function runPlanDelivery(deliveryName, options) {
|
|
|
4871
5694
|
}
|
|
4872
5695
|
function runPlanCapacity(trackArg, options) {
|
|
4873
5696
|
const root = resolveRepoRoot();
|
|
4874
|
-
const graph =
|
|
5697
|
+
const graph = load_graph7(coopDir(root));
|
|
4875
5698
|
const track = normalizeTrack(trackArg);
|
|
4876
5699
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
4877
5700
|
if (deliveries.length === 0) {
|
|
@@ -4911,17 +5734,43 @@ function runPlanCapacity(trackArg, options) {
|
|
|
4911
5734
|
}
|
|
4912
5735
|
function registerPlanCommand(program) {
|
|
4913
5736
|
const plan = program.command("plan").description("Planning commands");
|
|
4914
|
-
plan.command("delivery").description("Analyze delivery feasibility").argument("
|
|
4915
|
-
await runPlanDelivery(name, options);
|
|
5737
|
+
plan.command("delivery").description("Analyze delivery feasibility").argument("[name]", "Delivery id or name").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--monte-carlo", "Run Monte Carlo schedule forecast").option("--iterations <n>", "Monte Carlo iterations (default 10000)").option("--workers <n>", "Worker count hint (default 4)").option("--without <taskId>", "Remove a task and its exclusive dependencies from scope").option("--add-member <trackOrProfile>", "Add one default-capacity member to a human profile").option("--target <date>", "Run scenario with a different target date (YYYY-MM-DD)").option("--set <override>", "Override one field, for example TASK-1:priority=p0").action(async (name, options) => {
|
|
5738
|
+
await runPlanDelivery(name ?? "", options);
|
|
4916
5739
|
});
|
|
4917
5740
|
plan.command("capacity").description("Show track capacity utilization across deliveries").argument("<track>", "Track id").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((track, options) => {
|
|
4918
5741
|
runPlanCapacity(track, options);
|
|
4919
5742
|
});
|
|
4920
5743
|
}
|
|
4921
5744
|
|
|
5745
|
+
// src/commands/promote.ts
|
|
5746
|
+
function registerPromoteCommand(program) {
|
|
5747
|
+
program.command("promote").description("Promote a task within the current working context").argument("<id-or-type>", "Task id/alias, or the literal `task` followed by an id").argument("[id]", "Task id when an explicit entity type is provided").option("--track <track>", "Scoped track context").option("--version <version>", "Scoped version context").action((first, second, options) => {
|
|
5748
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
5749
|
+
if (entity !== "task") {
|
|
5750
|
+
throw new Error("Only task promotion is supported.");
|
|
5751
|
+
}
|
|
5752
|
+
const root = resolveRepoRoot();
|
|
5753
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5754
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track"));
|
|
5755
|
+
const resolvedVersion = resolveContextValueWithSource(options.version, context.version, sharedDefault(root, "version"));
|
|
5756
|
+
if (isVerboseRequested()) {
|
|
5757
|
+
for (const line of formatResolvedContextMessage({ track: resolvedTrack, version: resolvedVersion })) {
|
|
5758
|
+
console.log(line);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
5762
|
+
const task = promoteTaskForContext(parsed.task, {
|
|
5763
|
+
track: resolvedTrack.value,
|
|
5764
|
+
version: resolvedVersion.value
|
|
5765
|
+
});
|
|
5766
|
+
writeTaskEntry(filePath, parsed, task);
|
|
5767
|
+
console.log(`Promoted ${task.id}`);
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
|
|
4922
5771
|
// src/commands/project.ts
|
|
4923
|
-
import
|
|
4924
|
-
import
|
|
5772
|
+
import fs13 from "fs";
|
|
5773
|
+
import path18 from "path";
|
|
4925
5774
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
|
|
4926
5775
|
var TASK_TEMPLATE2 = `---
|
|
4927
5776
|
id: TASK-001
|
|
@@ -5030,11 +5879,11 @@ var PROJECT_DIRS = [
|
|
|
5030
5879
|
".index"
|
|
5031
5880
|
];
|
|
5032
5881
|
function ensureDir2(dirPath) {
|
|
5033
|
-
|
|
5882
|
+
fs13.mkdirSync(dirPath, { recursive: true });
|
|
5034
5883
|
}
|
|
5035
5884
|
function writeIfMissing3(filePath, content) {
|
|
5036
|
-
if (!
|
|
5037
|
-
|
|
5885
|
+
if (!fs13.existsSync(filePath)) {
|
|
5886
|
+
fs13.writeFileSync(filePath, content, "utf8");
|
|
5038
5887
|
}
|
|
5039
5888
|
}
|
|
5040
5889
|
function normalizeProjectId3(value) {
|
|
@@ -5042,17 +5891,17 @@ function normalizeProjectId3(value) {
|
|
|
5042
5891
|
}
|
|
5043
5892
|
function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID_NAMING_TEMPLATE) {
|
|
5044
5893
|
const workspaceDir = coopWorkspaceDir(root);
|
|
5045
|
-
const projectRoot =
|
|
5046
|
-
ensureDir2(
|
|
5894
|
+
const projectRoot = path18.join(workspaceDir, "projects", projectId);
|
|
5895
|
+
ensureDir2(path18.join(workspaceDir, "projects"));
|
|
5047
5896
|
for (const dir of PROJECT_DIRS) {
|
|
5048
|
-
ensureDir2(
|
|
5897
|
+
ensureDir2(path18.join(projectRoot, dir));
|
|
5049
5898
|
}
|
|
5050
|
-
writeIfMissing3(
|
|
5051
|
-
if (!
|
|
5899
|
+
writeIfMissing3(path18.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
|
|
5900
|
+
if (!fs13.existsSync(path18.join(projectRoot, "schema-version"))) {
|
|
5052
5901
|
write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
|
|
5053
5902
|
}
|
|
5054
|
-
writeIfMissing3(
|
|
5055
|
-
writeIfMissing3(
|
|
5903
|
+
writeIfMissing3(path18.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
|
|
5904
|
+
writeIfMissing3(path18.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
|
|
5056
5905
|
return projectRoot;
|
|
5057
5906
|
}
|
|
5058
5907
|
function registerProjectCommand(program) {
|
|
@@ -5080,7 +5929,7 @@ function registerProjectCommand(program) {
|
|
|
5080
5929
|
}
|
|
5081
5930
|
console.log(`id=${active.id}`);
|
|
5082
5931
|
console.log(`name=${active.name}`);
|
|
5083
|
-
console.log(`path=${
|
|
5932
|
+
console.log(`path=${path18.relative(root, active.root)}`);
|
|
5084
5933
|
console.log(`layout=${active.layout}`);
|
|
5085
5934
|
});
|
|
5086
5935
|
project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
|
|
@@ -5112,26 +5961,127 @@ function registerProjectCommand(program) {
|
|
|
5112
5961
|
version: 2,
|
|
5113
5962
|
current_project: workspace.current_project || projectId
|
|
5114
5963
|
});
|
|
5115
|
-
console.log(`Created project '${projectId}' at ${
|
|
5964
|
+
console.log(`Created project '${projectId}' at ${path18.relative(root, projectRoot)}`);
|
|
5965
|
+
});
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
// src/commands/prompt.ts
|
|
5969
|
+
import fs14 from "fs";
|
|
5970
|
+
function buildPayload(root, id) {
|
|
5971
|
+
const { parsed } = loadTaskEntry(root, id);
|
|
5972
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5973
|
+
const task = parsed.task;
|
|
5974
|
+
return {
|
|
5975
|
+
task: {
|
|
5976
|
+
id: task.id,
|
|
5977
|
+
title: task.title,
|
|
5978
|
+
status: task.status,
|
|
5979
|
+
type: task.type,
|
|
5980
|
+
track: task.track ?? null,
|
|
5981
|
+
delivery_tracks: task.delivery_tracks ?? [],
|
|
5982
|
+
delivery: task.delivery ?? null,
|
|
5983
|
+
priority: task.priority ?? null,
|
|
5984
|
+
effective_priority: taskEffectivePriority(task, context.track),
|
|
5985
|
+
fix_versions: task.fix_versions ?? [],
|
|
5986
|
+
released_in: task.released_in ?? [],
|
|
5987
|
+
acceptance: task.acceptance ?? [],
|
|
5988
|
+
tests_required: task.tests_required ?? [],
|
|
5989
|
+
refs: {
|
|
5990
|
+
authority: task.origin?.authority_refs ?? [],
|
|
5991
|
+
derived: task.origin?.derived_refs ?? []
|
|
5992
|
+
},
|
|
5993
|
+
execution: task.execution ?? null
|
|
5994
|
+
},
|
|
5995
|
+
working_context: context,
|
|
5996
|
+
body: parsed.body.trim()
|
|
5997
|
+
};
|
|
5998
|
+
}
|
|
5999
|
+
function renderMarkdown(payload) {
|
|
6000
|
+
const lines = [
|
|
6001
|
+
`# Task Prompt: ${payload.task.id}`,
|
|
6002
|
+
"",
|
|
6003
|
+
`- Title: ${payload.task.title}`,
|
|
6004
|
+
`- Status: ${payload.task.status}`,
|
|
6005
|
+
`- Type: ${payload.task.type}`,
|
|
6006
|
+
`- Home track: ${payload.task.track ?? "-"}`,
|
|
6007
|
+
`- Delivery tracks: ${payload.task.delivery_tracks.length > 0 ? payload.task.delivery_tracks.join(", ") : "-"}`,
|
|
6008
|
+
`- Delivery: ${payload.task.delivery ?? "-"}`,
|
|
6009
|
+
`- Effective priority: ${payload.task.effective_priority}`,
|
|
6010
|
+
`- Fix versions: ${payload.task.fix_versions.length > 0 ? payload.task.fix_versions.join(", ") : "-"}`,
|
|
6011
|
+
`- Released in: ${payload.task.released_in.length > 0 ? payload.task.released_in.join(", ") : "-"}`,
|
|
6012
|
+
"",
|
|
6013
|
+
"## Acceptance",
|
|
6014
|
+
...payload.task.acceptance.length > 0 ? payload.task.acceptance.map((entry) => `- ${entry}`) : ["- none"],
|
|
6015
|
+
"",
|
|
6016
|
+
"## Tests Required",
|
|
6017
|
+
...payload.task.tests_required.length > 0 ? payload.task.tests_required.map((entry) => `- ${entry}`) : ["- none"],
|
|
6018
|
+
"",
|
|
6019
|
+
"## Refs",
|
|
6020
|
+
`- Authority: ${payload.task.refs.authority.length > 0 ? payload.task.refs.authority.join(", ") : "-"}`,
|
|
6021
|
+
`- Derived: ${payload.task.refs.derived.length > 0 ? payload.task.refs.derived.join(", ") : "-"}`,
|
|
6022
|
+
"",
|
|
6023
|
+
"## Working Context",
|
|
6024
|
+
`- Track: ${payload.working_context.track ?? "-"}`,
|
|
6025
|
+
`- Delivery: ${payload.working_context.delivery ?? "-"}`,
|
|
6026
|
+
`- Version: ${payload.working_context.version ?? "-"}`,
|
|
6027
|
+
"",
|
|
6028
|
+
"## Task Body",
|
|
6029
|
+
payload.body || "-"
|
|
6030
|
+
];
|
|
6031
|
+
if (payload.task.execution) {
|
|
6032
|
+
lines.push("", "## Execution Hints", "```json", JSON.stringify(payload.task.execution, null, 2), "```");
|
|
6033
|
+
}
|
|
6034
|
+
return `${lines.join("\n")}
|
|
6035
|
+
`;
|
|
6036
|
+
}
|
|
6037
|
+
function registerPromptCommand(program) {
|
|
6038
|
+
program.command("prompt").description("Generate a manual agent prompt from a task").argument("<id-or-type>", "Task id/alias, or the literal `task` followed by an id").argument("[id]", "Task id when an explicit entity type is provided").option("--format <format>", "text|markdown|json", "text").option("--save <path>", "Write the prompt to a file as well as stdout").action((first, second, options) => {
|
|
6039
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
6040
|
+
if (entity !== "task") {
|
|
6041
|
+
throw new Error("Only task prompts are supported.");
|
|
6042
|
+
}
|
|
6043
|
+
const root = resolveRepoRoot();
|
|
6044
|
+
const payload = buildPayload(root, id);
|
|
6045
|
+
const format = options.format ?? "text";
|
|
6046
|
+
let output2 = "";
|
|
6047
|
+
if (format === "json") {
|
|
6048
|
+
output2 = `${JSON.stringify(payload, null, 2)}
|
|
6049
|
+
`;
|
|
6050
|
+
} else {
|
|
6051
|
+
output2 = renderMarkdown(payload);
|
|
6052
|
+
}
|
|
6053
|
+
if (options.save) {
|
|
6054
|
+
fs14.writeFileSync(options.save, output2, "utf8");
|
|
6055
|
+
}
|
|
6056
|
+
if (isVerboseRequested()) {
|
|
6057
|
+
for (const line of formatResolvedContextMessage({
|
|
6058
|
+
track: payload.working_context.track ? { value: payload.working_context.track, source: "use" } : void 0,
|
|
6059
|
+
delivery: payload.working_context.delivery ? { value: payload.working_context.delivery, source: "use" } : void 0,
|
|
6060
|
+
version: payload.working_context.version ? { value: payload.working_context.version, source: "use" } : void 0
|
|
6061
|
+
})) {
|
|
6062
|
+
console.log(line);
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
console.log(output2.trimEnd());
|
|
5116
6066
|
});
|
|
5117
6067
|
}
|
|
5118
6068
|
|
|
5119
6069
|
// src/commands/refine.ts
|
|
5120
|
-
import
|
|
5121
|
-
import
|
|
5122
|
-
import { parseIdeaFile as
|
|
6070
|
+
import fs15 from "fs";
|
|
6071
|
+
import path19 from "path";
|
|
6072
|
+
import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
|
|
5123
6073
|
import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
|
|
5124
|
-
function
|
|
6074
|
+
function resolveTaskFile2(root, idOrAlias) {
|
|
5125
6075
|
const target = resolveReference(root, idOrAlias, "task");
|
|
5126
|
-
return
|
|
6076
|
+
return path19.join(root, ...target.file.split("/"));
|
|
5127
6077
|
}
|
|
5128
|
-
function
|
|
6078
|
+
function resolveIdeaFile3(root, idOrAlias) {
|
|
5129
6079
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
5130
|
-
return
|
|
6080
|
+
return path19.join(root, ...target.file.split("/"));
|
|
5131
6081
|
}
|
|
5132
6082
|
async function readSupplementalInput(root, options) {
|
|
5133
6083
|
if (options.inputFile?.trim()) {
|
|
5134
|
-
return
|
|
6084
|
+
return fs15.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
|
|
5135
6085
|
}
|
|
5136
6086
|
if (options.stdin) {
|
|
5137
6087
|
return readStdinText();
|
|
@@ -5149,11 +6099,11 @@ function loadAuthorityContext(root, refs) {
|
|
|
5149
6099
|
for (const ref of refs ?? []) {
|
|
5150
6100
|
const filePart = extractRefFile(ref);
|
|
5151
6101
|
if (!filePart) continue;
|
|
5152
|
-
const fullPath =
|
|
5153
|
-
if (!
|
|
6102
|
+
const fullPath = path19.resolve(root, filePart);
|
|
6103
|
+
if (!fs15.existsSync(fullPath) || !fs15.statSync(fullPath).isFile()) continue;
|
|
5154
6104
|
out.push({
|
|
5155
6105
|
ref,
|
|
5156
|
-
content:
|
|
6106
|
+
content: fs15.readFileSync(fullPath, "utf8")
|
|
5157
6107
|
});
|
|
5158
6108
|
}
|
|
5159
6109
|
return out;
|
|
@@ -5163,8 +6113,8 @@ function registerRefineCommand(program) {
|
|
|
5163
6113
|
refine.command("idea").description("Refine an idea into proposed tasks").argument("<id>", "Idea ID or alias").option("--apply", "Apply the generated draft immediately").option("--output-file <path>", "Write the draft to a specific file").option("--input-file <path>", "Supplemental planning brief").option("--stdin", "Read supplemental planning brief from stdin").action(async (id, options) => {
|
|
5164
6114
|
const root = resolveRepoRoot();
|
|
5165
6115
|
const projectDir = ensureCoopInitialized(root);
|
|
5166
|
-
const ideaFile =
|
|
5167
|
-
const parsed =
|
|
6116
|
+
const ideaFile = resolveIdeaFile3(root, id);
|
|
6117
|
+
const parsed = parseIdeaFile5(ideaFile);
|
|
5168
6118
|
const supplemental = await readSupplementalInput(root, options);
|
|
5169
6119
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
5170
6120
|
const refined = await refine_idea_to_draft(
|
|
@@ -5187,8 +6137,8 @@ function registerRefineCommand(program) {
|
|
|
5187
6137
|
refine.command("task").description("Refine a task into an execution-ready update draft").argument("<id>", "Task ID or alias").option("--apply", "Apply the generated draft immediately").option("--output-file <path>", "Write the draft to a specific file").option("--input-file <path>", "Supplemental planning brief").option("--stdin", "Read supplemental planning brief from stdin").action(async (id, options) => {
|
|
5188
6138
|
const root = resolveRepoRoot();
|
|
5189
6139
|
const projectDir = ensureCoopInitialized(root);
|
|
5190
|
-
const taskFile =
|
|
5191
|
-
const parsed =
|
|
6140
|
+
const taskFile = resolveTaskFile2(root, id);
|
|
6141
|
+
const parsed = parseTaskFile11(taskFile);
|
|
5192
6142
|
const supplemental = await readSupplementalInput(root, options);
|
|
5193
6143
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
5194
6144
|
const draft = await refine_task_to_draft(
|
|
@@ -5216,15 +6166,15 @@ function registerRefineCommand(program) {
|
|
|
5216
6166
|
const written = applyRefinementDraft(root, projectDir, draft);
|
|
5217
6167
|
console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
|
|
5218
6168
|
for (const filePath of written) {
|
|
5219
|
-
console.log(`- ${
|
|
6169
|
+
console.log(`- ${path19.relative(root, filePath)}`);
|
|
5220
6170
|
}
|
|
5221
6171
|
});
|
|
5222
6172
|
}
|
|
5223
6173
|
|
|
5224
6174
|
// src/commands/run.ts
|
|
5225
|
-
import
|
|
5226
|
-
import
|
|
5227
|
-
import { load_graph as
|
|
6175
|
+
import fs16 from "fs";
|
|
6176
|
+
import path20 from "path";
|
|
6177
|
+
import { load_graph as load_graph8, parseTaskFile as parseTaskFile12 } from "@kitsy/coop-core";
|
|
5228
6178
|
import {
|
|
5229
6179
|
build_contract,
|
|
5230
6180
|
create_provider_agent_client,
|
|
@@ -5233,11 +6183,11 @@ import {
|
|
|
5233
6183
|
} from "@kitsy/coop-ai";
|
|
5234
6184
|
function loadTask(root, idOrAlias) {
|
|
5235
6185
|
const target = resolveReference(root, idOrAlias, "task");
|
|
5236
|
-
const taskFile =
|
|
5237
|
-
if (!
|
|
6186
|
+
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
6187
|
+
if (!fs16.existsSync(taskFile)) {
|
|
5238
6188
|
throw new Error(`Task file not found: ${target.file}`);
|
|
5239
6189
|
}
|
|
5240
|
-
return
|
|
6190
|
+
return parseTaskFile12(taskFile).task;
|
|
5241
6191
|
}
|
|
5242
6192
|
function printContract(contract) {
|
|
5243
6193
|
console.log(JSON.stringify(contract, null, 2));
|
|
@@ -5250,7 +6200,7 @@ function registerRunCommand(program) {
|
|
|
5250
6200
|
run.command("task").description("Execute a task runbook").argument("<id>", "Task ID or alias").option("--step <step>", "Run a single step by step id").option("--dry-run", "Print contract without executing").action(async (id, options) => {
|
|
5251
6201
|
const root = resolveRepoRoot();
|
|
5252
6202
|
const coop = ensureCoopInitialized(root);
|
|
5253
|
-
const graph =
|
|
6203
|
+
const graph = load_graph8(coopDir(root));
|
|
5254
6204
|
const task = loadTask(root, id);
|
|
5255
6205
|
const config = readCoopConfig(root).raw;
|
|
5256
6206
|
const routedAgent = select_agent(task, config);
|
|
@@ -5273,26 +6223,141 @@ function registerRunCommand(program) {
|
|
|
5273
6223
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
5274
6224
|
});
|
|
5275
6225
|
if (result.status === "failed") {
|
|
5276
|
-
throw new Error(`Run failed: ${result.run.id}. Log: ${
|
|
6226
|
+
throw new Error(`Run failed: ${result.run.id}. Log: ${path20.relative(root, result.log_path)}`);
|
|
5277
6227
|
}
|
|
5278
6228
|
if (result.status === "paused") {
|
|
5279
6229
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
5280
|
-
console.log(`[COOP] log: ${
|
|
6230
|
+
console.log(`[COOP] log: ${path20.relative(root, result.log_path)}`);
|
|
5281
6231
|
return;
|
|
5282
6232
|
}
|
|
5283
6233
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
5284
|
-
console.log(`[COOP] log: ${
|
|
6234
|
+
console.log(`[COOP] log: ${path20.relative(root, result.log_path)}`);
|
|
6235
|
+
});
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
// src/commands/search.ts
|
|
6239
|
+
import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile3, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
|
|
6240
|
+
function haystackForTask(task) {
|
|
6241
|
+
return [
|
|
6242
|
+
task.id,
|
|
6243
|
+
task.title,
|
|
6244
|
+
...task.aliases ?? [],
|
|
6245
|
+
...task.tags ?? [],
|
|
6246
|
+
...task.acceptance ?? [],
|
|
6247
|
+
...task.tests_required ?? [],
|
|
6248
|
+
...task.fix_versions ?? [],
|
|
6249
|
+
...task.released_in ?? [],
|
|
6250
|
+
...task.delivery_tracks ?? [],
|
|
6251
|
+
...task.origin?.authority_refs ?? [],
|
|
6252
|
+
...task.origin?.derived_refs ?? [],
|
|
6253
|
+
...task.comments?.map((entry) => entry.body) ?? []
|
|
6254
|
+
].join("\n").toLowerCase();
|
|
6255
|
+
}
|
|
6256
|
+
function haystackForIdea(idea, body) {
|
|
6257
|
+
return [idea.id, idea.title, ...idea.aliases ?? [], ...idea.tags, ...idea.linked_tasks, body].join("\n").toLowerCase();
|
|
6258
|
+
}
|
|
6259
|
+
function haystackForDelivery(delivery, body) {
|
|
6260
|
+
return [
|
|
6261
|
+
delivery.id,
|
|
6262
|
+
delivery.name,
|
|
6263
|
+
delivery.status,
|
|
6264
|
+
...delivery.scope.include ?? [],
|
|
6265
|
+
...delivery.scope.exclude ?? [],
|
|
6266
|
+
...delivery.capacity_profiles ?? [],
|
|
6267
|
+
body
|
|
6268
|
+
].join("\n").toLowerCase();
|
|
6269
|
+
}
|
|
6270
|
+
function includesQuery(haystack, query) {
|
|
6271
|
+
return haystack.includes(query.toLowerCase());
|
|
6272
|
+
}
|
|
6273
|
+
function registerSearchCommand(program) {
|
|
6274
|
+
program.command("search").description("Deterministic text search across tasks, ideas, and deliveries").argument("<query>", "Search text").option("--kind <kind>", "task|idea|delivery|all", "all").option("--track <id>", "Filter tasks by home/contributing track").option("--delivery <id>", "Filter tasks by delivery").option("--version <id>", "Filter tasks by fix/released version").option("--status <status>", "Filter by item status").option("--limit <n>", "Maximum result count", "20").option("--open", "Require exactly one match and print only that resolved summary row").action((query, options) => {
|
|
6275
|
+
const root = resolveRepoRoot();
|
|
6276
|
+
const graph = load_graph9(coopDir(root));
|
|
6277
|
+
const deliveryScope = options.delivery ? new Set(graph.deliveries.get(options.delivery)?.scope.include ?? []) : null;
|
|
6278
|
+
const limit = Number(options.limit ?? "20");
|
|
6279
|
+
const rows = [];
|
|
6280
|
+
if (options.kind === "all" || options.kind === "task" || !options.kind) {
|
|
6281
|
+
for (const filePath of listTaskFiles(root)) {
|
|
6282
|
+
const parsed = parseTaskFile13(filePath);
|
|
6283
|
+
const task = parsed.task;
|
|
6284
|
+
if (options.status && task.status !== options.status) continue;
|
|
6285
|
+
if (options.track && task.track !== options.track && !(task.delivery_tracks ?? []).includes(options.track)) {
|
|
6286
|
+
continue;
|
|
6287
|
+
}
|
|
6288
|
+
if (options.delivery && task.delivery !== options.delivery && !deliveryScope?.has(task.id)) continue;
|
|
6289
|
+
if (options.version && !(task.fix_versions ?? []).includes(options.version) && !(task.released_in ?? []).includes(options.version)) {
|
|
6290
|
+
continue;
|
|
6291
|
+
}
|
|
6292
|
+
if (!includesQuery(`${haystackForTask(task)}
|
|
6293
|
+
${parsed.body}`, query)) continue;
|
|
6294
|
+
rows.push({
|
|
6295
|
+
kind: "task",
|
|
6296
|
+
id: task.id,
|
|
6297
|
+
title: task.title,
|
|
6298
|
+
status: task.status,
|
|
6299
|
+
extra: task.track ?? "-"
|
|
6300
|
+
});
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
if (options.kind === "all" || options.kind === "idea") {
|
|
6304
|
+
for (const filePath of listIdeaFiles(root)) {
|
|
6305
|
+
const parsed = parseIdeaFile6(filePath);
|
|
6306
|
+
if (options.status && parsed.idea.status !== options.status) continue;
|
|
6307
|
+
if (!includesQuery(haystackForIdea(parsed.idea, parsed.body), query)) continue;
|
|
6308
|
+
rows.push({
|
|
6309
|
+
kind: "idea",
|
|
6310
|
+
id: parsed.idea.id,
|
|
6311
|
+
title: parsed.idea.title,
|
|
6312
|
+
status: parsed.idea.status
|
|
6313
|
+
});
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
if (options.kind === "all" || options.kind === "delivery") {
|
|
6317
|
+
for (const filePath of listDeliveryFiles(root)) {
|
|
6318
|
+
const parsed = parseDeliveryFile3(filePath);
|
|
6319
|
+
if (options.status && parsed.delivery.status !== options.status) continue;
|
|
6320
|
+
if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
|
|
6321
|
+
rows.push({
|
|
6322
|
+
kind: "delivery",
|
|
6323
|
+
id: parsed.delivery.id,
|
|
6324
|
+
title: parsed.delivery.name,
|
|
6325
|
+
status: parsed.delivery.status,
|
|
6326
|
+
extra: parsed.delivery.target_date ?? "-"
|
|
6327
|
+
});
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
rows.sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`));
|
|
6331
|
+
if (options.open) {
|
|
6332
|
+
if (rows.length === 0) {
|
|
6333
|
+
console.log("No matches found.");
|
|
6334
|
+
return;
|
|
6335
|
+
}
|
|
6336
|
+
if (rows.length !== 1) {
|
|
6337
|
+
throw new Error(`--open requires exactly one match, found ${rows.length}. Narrow the query or add filters.`);
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
for (const row of rows.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 20)) {
|
|
6341
|
+
const suffix = row.extra ? ` | ${row.extra}` : "";
|
|
6342
|
+
console.log(`${row.kind} ${row.id} ${row.status} ${row.title}${suffix}`);
|
|
6343
|
+
if (options.open) {
|
|
6344
|
+
break;
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
if (rows.length === 0) {
|
|
6348
|
+
console.log("No matches found.");
|
|
6349
|
+
}
|
|
5285
6350
|
});
|
|
5286
6351
|
}
|
|
5287
6352
|
|
|
5288
6353
|
// src/server/api.ts
|
|
5289
|
-
import
|
|
6354
|
+
import fs17 from "fs";
|
|
5290
6355
|
import http2 from "http";
|
|
5291
|
-
import
|
|
6356
|
+
import path21 from "path";
|
|
5292
6357
|
import {
|
|
5293
6358
|
analyze_feasibility as analyze_feasibility2,
|
|
5294
|
-
load_graph as
|
|
5295
|
-
parseTaskFile as
|
|
6359
|
+
load_graph as load_graph10,
|
|
6360
|
+
parseTaskFile as parseTaskFile14,
|
|
5296
6361
|
resolve_external_dependencies
|
|
5297
6362
|
} from "@kitsy/coop-core";
|
|
5298
6363
|
function json(res, statusCode, payload) {
|
|
@@ -5334,12 +6399,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
5334
6399
|
};
|
|
5335
6400
|
}
|
|
5336
6401
|
function taskFileById(root, id) {
|
|
5337
|
-
const tasksDir =
|
|
5338
|
-
if (!
|
|
5339
|
-
const entries =
|
|
6402
|
+
const tasksDir = path21.join(resolveProject(root).root, "tasks");
|
|
6403
|
+
if (!fs17.existsSync(tasksDir)) return null;
|
|
6404
|
+
const entries = fs17.readdirSync(tasksDir, { withFileTypes: true });
|
|
5340
6405
|
const target = `${id}.md`.toLowerCase();
|
|
5341
6406
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
5342
|
-
return match ?
|
|
6407
|
+
return match ? path21.join(tasksDir, match.name) : null;
|
|
5343
6408
|
}
|
|
5344
6409
|
function loadRemoteConfig(root) {
|
|
5345
6410
|
const raw = readCoopConfig(root).raw;
|
|
@@ -5405,7 +6470,7 @@ function createApiServer(root, options = {}) {
|
|
|
5405
6470
|
}
|
|
5406
6471
|
const project = resolveProject(repoRoot);
|
|
5407
6472
|
const coopPath = project.root;
|
|
5408
|
-
const graph =
|
|
6473
|
+
const graph = load_graph10(coopPath);
|
|
5409
6474
|
const resolutions = await externalResolutions(repoRoot, graph, options);
|
|
5410
6475
|
if (pathname === "/api/tasks") {
|
|
5411
6476
|
const tasks = Array.from(graph.nodes.values()).sort((a, b) => a.id.localeCompare(b.id)).map((task) => taskSummary(graph, task, resolutions.get(task.id) ?? []));
|
|
@@ -5420,7 +6485,7 @@ function createApiServer(root, options = {}) {
|
|
|
5420
6485
|
notFound(res, `Task '${taskId}' not found.`);
|
|
5421
6486
|
return;
|
|
5422
6487
|
}
|
|
5423
|
-
const parsed =
|
|
6488
|
+
const parsed = parseTaskFile14(filePath);
|
|
5424
6489
|
const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
|
|
5425
6490
|
json(res, 200, {
|
|
5426
6491
|
...workspaceMeta(repoRoot),
|
|
@@ -5429,7 +6494,7 @@ function createApiServer(root, options = {}) {
|
|
|
5429
6494
|
created: task.created,
|
|
5430
6495
|
updated: task.updated,
|
|
5431
6496
|
body: parsed.body,
|
|
5432
|
-
file_path:
|
|
6497
|
+
file_path: path21.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
5433
6498
|
}
|
|
5434
6499
|
});
|
|
5435
6500
|
return;
|
|
@@ -5517,9 +6582,9 @@ function registerServeCommand(program) {
|
|
|
5517
6582
|
}
|
|
5518
6583
|
|
|
5519
6584
|
// src/commands/show.ts
|
|
5520
|
-
import
|
|
5521
|
-
import
|
|
5522
|
-
import { parseIdeaFile as
|
|
6585
|
+
import fs18 from "fs";
|
|
6586
|
+
import path22 from "path";
|
|
6587
|
+
import { parseIdeaFile as parseIdeaFile7 } from "@kitsy/coop-core";
|
|
5523
6588
|
function stringify(value) {
|
|
5524
6589
|
if (value === null || value === void 0) return "-";
|
|
5525
6590
|
if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "-";
|
|
@@ -5537,13 +6602,13 @@ function pushListSection(lines, title, values) {
|
|
|
5537
6602
|
}
|
|
5538
6603
|
}
|
|
5539
6604
|
function loadComputedFromIndex(root, taskId) {
|
|
5540
|
-
const indexPath =
|
|
5541
|
-
if (!
|
|
6605
|
+
const indexPath = path22.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
6606
|
+
if (!fs18.existsSync(indexPath)) {
|
|
5542
6607
|
return null;
|
|
5543
6608
|
}
|
|
5544
6609
|
let parsed;
|
|
5545
6610
|
try {
|
|
5546
|
-
parsed = JSON.parse(
|
|
6611
|
+
parsed = JSON.parse(fs18.readFileSync(indexPath, "utf8"));
|
|
5547
6612
|
} catch {
|
|
5548
6613
|
return null;
|
|
5549
6614
|
}
|
|
@@ -5578,30 +6643,59 @@ function loadComputedFromIndex(root, taskId) {
|
|
|
5578
6643
|
}
|
|
5579
6644
|
return null;
|
|
5580
6645
|
}
|
|
5581
|
-
function
|
|
6646
|
+
function formatTimeSummary(task) {
|
|
6647
|
+
const planned = task.time?.planned_hours;
|
|
6648
|
+
const worked = (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((sum, entry) => sum + entry.hours, 0);
|
|
6649
|
+
const plannedLogged = (task.time?.logs ?? []).filter((entry) => entry.kind === "planned").reduce((sum, entry) => sum + entry.hours, 0);
|
|
6650
|
+
return `planned_hours=${planned ?? "-"} | planned_logged=${plannedLogged || 0} | worked=${worked || 0}`;
|
|
6651
|
+
}
|
|
6652
|
+
function showTask(taskId, options = {}) {
|
|
5582
6653
|
const root = resolveRepoRoot();
|
|
5583
|
-
const
|
|
5584
|
-
const
|
|
5585
|
-
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5586
|
-
const parsed = parseTaskFile13(taskFile);
|
|
6654
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6655
|
+
const { filePath, parsed } = loadTaskEntry(root, taskId);
|
|
5587
6656
|
const task = parsed.task;
|
|
5588
6657
|
const body = parsed.body.trim();
|
|
5589
6658
|
const computed = loadComputedFromIndex(root, task.id);
|
|
6659
|
+
if (options.compact) {
|
|
6660
|
+
const compactLines = [
|
|
6661
|
+
`Task: ${task.id}`,
|
|
6662
|
+
`Title: ${task.title}`,
|
|
6663
|
+
`Status: ${task.status}`,
|
|
6664
|
+
`Priority: ${task.priority ?? "-"}`,
|
|
6665
|
+
`Effective Priority: ${taskEffectivePriority(task, context.track)}`,
|
|
6666
|
+
`Track: ${task.track ?? "-"}`,
|
|
6667
|
+
`Delivery: ${task.delivery ?? "-"}`,
|
|
6668
|
+
`Assignee: ${task.assignee ?? "-"}`,
|
|
6669
|
+
`Depends On: ${stringify(task.depends_on)}`,
|
|
6670
|
+
`Acceptance: ${task.acceptance && task.acceptance.length > 0 ? task.acceptance.join(" | ") : "-"}`,
|
|
6671
|
+
`Tests Required: ${task.tests_required && task.tests_required.length > 0 ? task.tests_required.join(", ") : "-"}`,
|
|
6672
|
+
`File: ${path22.relative(root, filePath)}`
|
|
6673
|
+
];
|
|
6674
|
+
console.log(compactLines.join("\n"));
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
5590
6677
|
const lines = [
|
|
5591
6678
|
`Task: ${task.id}`,
|
|
5592
6679
|
`Title: ${task.title}`,
|
|
5593
6680
|
`Status: ${task.status}`,
|
|
5594
6681
|
`Type: ${task.type}`,
|
|
5595
6682
|
`Priority: ${task.priority ?? "-"}`,
|
|
6683
|
+
`Effective Priority: ${taskEffectivePriority(task, context.track)}`,
|
|
5596
6684
|
`Track: ${task.track ?? "-"}`,
|
|
6685
|
+
`Delivery Tracks: ${stringify(task.delivery_tracks)}`,
|
|
6686
|
+
`Priority Context: ${stringify(task.priority_context)}`,
|
|
5597
6687
|
`Assignee: ${task.assignee ?? "-"}`,
|
|
5598
6688
|
`Delivery: ${task.delivery ?? "-"}`,
|
|
6689
|
+
`Fix Versions: ${stringify(task.fix_versions)}`,
|
|
6690
|
+
`Released In: ${stringify(task.released_in)}`,
|
|
6691
|
+
`Story Points: ${task.story_points ?? "-"}`,
|
|
6692
|
+
`Time: ${formatTimeSummary(task)}`,
|
|
5599
6693
|
`Aliases: ${stringify(task.aliases)}`,
|
|
5600
6694
|
`Depends On: ${stringify(task.depends_on)}`,
|
|
5601
6695
|
`Tags: ${stringify(task.tags)}`,
|
|
5602
6696
|
`Created: ${task.created}`,
|
|
5603
6697
|
`Updated: ${task.updated}`,
|
|
5604
|
-
`File: ${
|
|
6698
|
+
`File: ${path22.relative(root, filePath)}`,
|
|
5605
6699
|
""
|
|
5606
6700
|
];
|
|
5607
6701
|
pushListSection(lines, "Acceptance", task.acceptance);
|
|
@@ -5618,15 +6712,25 @@ function showTask(taskId) {
|
|
|
5618
6712
|
lines.push(`- Promoted To: ${stringify(task.origin.promoted_to)}`);
|
|
5619
6713
|
lines.push(`- Snapshot SHA256: ${task.origin.snapshot_sha256 ?? "-"}`);
|
|
5620
6714
|
}
|
|
5621
|
-
lines.push(
|
|
5622
|
-
|
|
5623
|
-
"
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
6715
|
+
lines.push("", "Comments:");
|
|
6716
|
+
if (!task.comments || task.comments.length === 0) {
|
|
6717
|
+
lines.push("- none");
|
|
6718
|
+
} else {
|
|
6719
|
+
for (const comment of task.comments) {
|
|
6720
|
+
lines.push(`- ${comment.at} ${comment.author}: ${comment.body}`);
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
lines.push("", "Time Logs:");
|
|
6724
|
+
if (!task.time?.logs || task.time.logs.length === 0) {
|
|
6725
|
+
lines.push("- none");
|
|
6726
|
+
} else {
|
|
6727
|
+
for (const entry of task.time.logs) {
|
|
6728
|
+
lines.push(`- ${entry.at} ${entry.actor} ${entry.kind} ${entry.hours}h${entry.note ? ` (${entry.note})` : ""}`);
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
lines.push("", "Body:", body || "-", "", "Computed:");
|
|
5628
6732
|
if (!computed) {
|
|
5629
|
-
lines.push(`index not built (${
|
|
6733
|
+
lines.push(`index not built (${path22.relative(root, path22.join(ensureCoopInitialized(root), ".index", "tasks.json"))} missing)`);
|
|
5630
6734
|
} else {
|
|
5631
6735
|
for (const [key, value] of Object.entries(computed)) {
|
|
5632
6736
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -5634,14 +6738,26 @@ function showTask(taskId) {
|
|
|
5634
6738
|
}
|
|
5635
6739
|
console.log(lines.join("\n"));
|
|
5636
6740
|
}
|
|
5637
|
-
function showIdea(ideaId) {
|
|
6741
|
+
function showIdea(ideaId, options = {}) {
|
|
5638
6742
|
const root = resolveRepoRoot();
|
|
5639
|
-
ensureCoopInitialized(root);
|
|
5640
6743
|
const target = resolveReference(root, ideaId, "idea");
|
|
5641
|
-
const ideaFile =
|
|
5642
|
-
const parsed =
|
|
6744
|
+
const ideaFile = path22.join(root, ...target.file.split("/"));
|
|
6745
|
+
const parsed = parseIdeaFile7(ideaFile);
|
|
5643
6746
|
const idea = parsed.idea;
|
|
5644
6747
|
const body = parsed.body.trim();
|
|
6748
|
+
if (options.compact) {
|
|
6749
|
+
console.log(
|
|
6750
|
+
[
|
|
6751
|
+
`Idea: ${idea.id}`,
|
|
6752
|
+
`Title: ${idea.title}`,
|
|
6753
|
+
`Status: ${idea.status}`,
|
|
6754
|
+
`Tags: ${stringify(idea.tags)}`,
|
|
6755
|
+
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
6756
|
+
`File: ${path22.relative(root, ideaFile)}`
|
|
6757
|
+
].join("\n")
|
|
6758
|
+
);
|
|
6759
|
+
return;
|
|
6760
|
+
}
|
|
5645
6761
|
const lines = [
|
|
5646
6762
|
`Idea: ${idea.id}`,
|
|
5647
6763
|
`Title: ${idea.title}`,
|
|
@@ -5652,29 +6768,81 @@ function showIdea(ideaId) {
|
|
|
5652
6768
|
`Tags: ${stringify(idea.tags)}`,
|
|
5653
6769
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
5654
6770
|
`Created: ${idea.created}`,
|
|
5655
|
-
`File: ${
|
|
6771
|
+
`File: ${path22.relative(root, ideaFile)}`,
|
|
5656
6772
|
"",
|
|
5657
6773
|
"Body:",
|
|
5658
6774
|
body || "-"
|
|
5659
6775
|
];
|
|
5660
6776
|
console.log(lines.join("\n"));
|
|
5661
6777
|
}
|
|
6778
|
+
function showDelivery(ref, options = {}) {
|
|
6779
|
+
const root = resolveRepoRoot();
|
|
6780
|
+
const { filePath, delivery, body } = resolveDeliveryEntry(root, ref);
|
|
6781
|
+
if (options.compact) {
|
|
6782
|
+
console.log(
|
|
6783
|
+
[
|
|
6784
|
+
`Delivery: ${delivery.id}`,
|
|
6785
|
+
`Name: ${delivery.name}`,
|
|
6786
|
+
`Status: ${delivery.status}`,
|
|
6787
|
+
`Target Date: ${delivery.target_date ?? "-"}`,
|
|
6788
|
+
`Scope Include: ${delivery.scope.include.length > 0 ? delivery.scope.include.join(", ") : "-"}`,
|
|
6789
|
+
`File: ${path22.relative(root, filePath)}`
|
|
6790
|
+
].join("\n")
|
|
6791
|
+
);
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
const lines = [
|
|
6795
|
+
`Delivery: ${delivery.id}`,
|
|
6796
|
+
`Name: ${delivery.name}`,
|
|
6797
|
+
`Status: ${delivery.status}`,
|
|
6798
|
+
`Target Date: ${delivery.target_date ?? "-"}`,
|
|
6799
|
+
`Started Date: ${delivery.started_date ?? "-"}`,
|
|
6800
|
+
`Delivered Date: ${delivery.delivered_date ?? "-"}`,
|
|
6801
|
+
`Capacity Profiles: ${delivery.capacity_profiles.length > 0 ? delivery.capacity_profiles.join(", ") : "-"}`,
|
|
6802
|
+
`Scope Include: ${delivery.scope.include.length > 0 ? delivery.scope.include.join(", ") : "-"}`,
|
|
6803
|
+
`Scope Exclude: ${delivery.scope.exclude.length > 0 ? delivery.scope.exclude.join(", ") : "-"}`,
|
|
6804
|
+
`File: ${path22.relative(root, filePath)}`,
|
|
6805
|
+
"",
|
|
6806
|
+
"Body:",
|
|
6807
|
+
body.trim() || "-"
|
|
6808
|
+
];
|
|
6809
|
+
console.log(lines.join("\n"));
|
|
6810
|
+
}
|
|
6811
|
+
function showByReference(ref, options = {}) {
|
|
6812
|
+
const root = resolveRepoRoot();
|
|
6813
|
+
try {
|
|
6814
|
+
const resolved = resolveReference(root, ref);
|
|
6815
|
+
if (resolved.type === "task") {
|
|
6816
|
+
showTask(ref, options);
|
|
6817
|
+
return;
|
|
6818
|
+
}
|
|
6819
|
+
showIdea(ref, options);
|
|
6820
|
+
return;
|
|
6821
|
+
} catch {
|
|
6822
|
+
showDelivery(ref, options);
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
5662
6825
|
function registerShowCommand(program) {
|
|
5663
|
-
const show = program.command("show").description("Show detailed COOP entities")
|
|
5664
|
-
|
|
5665
|
-
|
|
6826
|
+
const show = program.command("show").description("Show detailed COOP entities").argument("[ref]", "Task, idea, or delivery reference").option("--compact", "Show a smaller summary view").action((ref, options) => {
|
|
6827
|
+
if (!ref?.trim()) {
|
|
6828
|
+
throw new Error("Provide a task, idea, or delivery reference.");
|
|
6829
|
+
}
|
|
6830
|
+
showByReference(ref, options);
|
|
5666
6831
|
});
|
|
5667
|
-
show.command("
|
|
5668
|
-
|
|
6832
|
+
show.command("task").description("Show task details").argument("<id>", "Task ID").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
6833
|
+
showTask(id, options);
|
|
5669
6834
|
});
|
|
5670
|
-
show.command("
|
|
5671
|
-
|
|
6835
|
+
show.command("idea").description("Show idea details").argument("<id>", "Idea ID").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
6836
|
+
showIdea(id, options);
|
|
6837
|
+
});
|
|
6838
|
+
show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
6839
|
+
showDelivery(id, options);
|
|
5672
6840
|
});
|
|
5673
6841
|
}
|
|
5674
6842
|
|
|
5675
6843
|
// src/commands/status.ts
|
|
5676
6844
|
import chalk4 from "chalk";
|
|
5677
|
-
import { TaskStatus as TaskStatus4, analyze_feasibility as analyze_feasibility3, load_graph as
|
|
6845
|
+
import { TaskStatus as TaskStatus4, analyze_feasibility as analyze_feasibility3, load_graph as load_graph11 } from "@kitsy/coop-core";
|
|
5678
6846
|
function countBy(values, keyFn) {
|
|
5679
6847
|
const out = /* @__PURE__ */ new Map();
|
|
5680
6848
|
for (const value of values) {
|
|
@@ -5695,10 +6863,16 @@ function completionSummary(done, totalTasks) {
|
|
|
5695
6863
|
const percent = totalTasks > 0 ? Math.round(done / totalTasks * 100) : 0;
|
|
5696
6864
|
return `${done}/${totalTasks} (${percent}%)`;
|
|
5697
6865
|
}
|
|
6866
|
+
function workedHours(tasks) {
|
|
6867
|
+
return tasks.reduce(
|
|
6868
|
+
(sum, task) => sum + (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((entrySum, entry) => entrySum + entry.hours, 0),
|
|
6869
|
+
0
|
|
6870
|
+
);
|
|
6871
|
+
}
|
|
5698
6872
|
function registerStatusCommand(program) {
|
|
5699
6873
|
program.command("status").description("Project dashboard overview").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
|
|
5700
6874
|
const root = resolveRepoRoot();
|
|
5701
|
-
const graph =
|
|
6875
|
+
const graph = load_graph11(coopDir(root));
|
|
5702
6876
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
5703
6877
|
const tasks = Array.from(graph.nodes.values());
|
|
5704
6878
|
const tasksByStatus = countBy(tasks, (task) => task.status);
|
|
@@ -5718,6 +6892,14 @@ function registerStatusCommand(program) {
|
|
|
5718
6892
|
const trackRows = Array.from(tasksByTrack.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([track, count]) => [track, String(count)]);
|
|
5719
6893
|
lines.push(formatTable(["Track", "Count"], trackRows));
|
|
5720
6894
|
lines.push("");
|
|
6895
|
+
const totalStoryPoints = tasks.reduce((sum, task) => sum + (task.story_points ?? 0), 0);
|
|
6896
|
+
const totalPlannedHours = tasks.reduce((sum, task) => sum + (task.time?.planned_hours ?? 0), 0);
|
|
6897
|
+
const totalWorkedHours = workedHours(tasks);
|
|
6898
|
+
lines.push(chalk4.bold("Effort Snapshot"));
|
|
6899
|
+
lines.push(`Story points: ${totalStoryPoints}`);
|
|
6900
|
+
lines.push(`Planned hours: ${totalPlannedHours.toFixed(2)}`);
|
|
6901
|
+
lines.push(`Worked hours: ${totalWorkedHours.toFixed(2)}`);
|
|
6902
|
+
lines.push("");
|
|
5721
6903
|
lines.push(chalk4.bold("Delivery Health"));
|
|
5722
6904
|
const deliveryRows = [];
|
|
5723
6905
|
const riskLines = [];
|
|
@@ -5769,8 +6951,8 @@ function registerTransitionCommand(program) {
|
|
|
5769
6951
|
}
|
|
5770
6952
|
|
|
5771
6953
|
// src/commands/taskflow.ts
|
|
5772
|
-
import
|
|
5773
|
-
import { parseTaskFile as
|
|
6954
|
+
import path23 from "path";
|
|
6955
|
+
import { parseTaskFile as parseTaskFile15 } from "@kitsy/coop-core";
|
|
5774
6956
|
function normalizeExecutor(value) {
|
|
5775
6957
|
if (!value?.trim()) return void 0;
|
|
5776
6958
|
const normalized = value.trim().toLowerCase();
|
|
@@ -5807,8 +6989,8 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5807
6989
|
}
|
|
5808
6990
|
}
|
|
5809
6991
|
const reference = resolveReference(root, taskId, "task");
|
|
5810
|
-
const filePath =
|
|
5811
|
-
const parsed =
|
|
6992
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
6993
|
+
const parsed = parseTaskFile15(filePath);
|
|
5812
6994
|
if (parsed.task.status === "in_progress") {
|
|
5813
6995
|
console.log(`Task ${parsed.task.id} is already in_progress.`);
|
|
5814
6996
|
return;
|
|
@@ -5820,10 +7002,37 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5820
7002
|
});
|
|
5821
7003
|
console.log(`Updated ${transitioned.task.id}: ${transitioned.from} -> ${transitioned.to}`);
|
|
5822
7004
|
}
|
|
7005
|
+
function maybePromote(root, taskId, options) {
|
|
7006
|
+
if (!options.promote) {
|
|
7007
|
+
return;
|
|
7008
|
+
}
|
|
7009
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
7010
|
+
const { filePath, parsed } = loadTaskEntry(root, taskId);
|
|
7011
|
+
const promoted = promoteTaskForContext(parsed.task, {
|
|
7012
|
+
track: options.track ?? context.track,
|
|
7013
|
+
version: options.version ?? context.version
|
|
7014
|
+
});
|
|
7015
|
+
writeTaskEntry(filePath, parsed, promoted);
|
|
7016
|
+
console.log(`Promoted ${promoted.id}`);
|
|
7017
|
+
}
|
|
7018
|
+
function printResolvedSelectionContext(root, options) {
|
|
7019
|
+
if (!isVerboseRequested()) {
|
|
7020
|
+
return;
|
|
7021
|
+
}
|
|
7022
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
7023
|
+
for (const line of formatResolvedContextMessage({
|
|
7024
|
+
track: resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track")),
|
|
7025
|
+
delivery: resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery")),
|
|
7026
|
+
version: resolveContextValueWithSource(options.version, context.version, sharedDefault(root, "version"))
|
|
7027
|
+
})) {
|
|
7028
|
+
console.log(line);
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
5823
7031
|
function registerTaskFlowCommands(program) {
|
|
5824
7032
|
const next = program.command("next").description("Select the next COOP work item");
|
|
5825
|
-
next.command("task").description("Show the top ready task").option("--track <track>", "Filter by track, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
|
|
7033
|
+
next.command("task").description("Show the top ready task").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
|
|
5826
7034
|
const root = resolveRepoRoot();
|
|
7035
|
+
printResolvedSelectionContext(root, options);
|
|
5827
7036
|
const selected = selectTopReadyTask(root, {
|
|
5828
7037
|
track: options.track,
|
|
5829
7038
|
delivery: options.delivery,
|
|
@@ -5833,29 +7042,49 @@ function registerTaskFlowCommands(program) {
|
|
|
5833
7042
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
5834
7043
|
});
|
|
5835
7044
|
const pick = program.command("pick").description("Pick the next COOP work item");
|
|
5836
|
-
pick.command("task").description("Select the top ready task and move it into active work").option("--track <track>", "Filter by track, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--to <assignee>", "Assign the selected task before starting it").option("--claim", "Assign the selected task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (options) => {
|
|
7045
|
+
pick.command("task").description("Select the top ready task and move it into active work").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid)").option("--today <date>", "Evaluation date (YYYY-MM-DD)").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the selected task before starting it").option("--claim", "Assign the selected task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
|
|
5837
7046
|
const root = resolveRepoRoot();
|
|
5838
|
-
|
|
7047
|
+
printResolvedSelectionContext(root, options);
|
|
7048
|
+
const selected = id?.trim() ? {
|
|
7049
|
+
entry: {
|
|
7050
|
+
task: loadTaskEntry(root, id).parsed.task,
|
|
7051
|
+
score: 0,
|
|
7052
|
+
readiness: statusToReadiness(loadTaskEntry(root, id).parsed.task.status),
|
|
7053
|
+
fits_capacity: true,
|
|
7054
|
+
fits_wip: true
|
|
7055
|
+
},
|
|
7056
|
+
selection: {
|
|
7057
|
+
track: options.track,
|
|
7058
|
+
delivery: options.delivery,
|
|
7059
|
+
version: options.version,
|
|
7060
|
+
executor: normalizeExecutor(options.executor),
|
|
7061
|
+
today: options.today
|
|
7062
|
+
}
|
|
7063
|
+
} : selectTopReadyTask(root, {
|
|
5839
7064
|
track: options.track,
|
|
5840
7065
|
delivery: options.delivery,
|
|
7066
|
+
version: options.version,
|
|
5841
7067
|
executor: normalizeExecutor(options.executor),
|
|
5842
7068
|
today: options.today
|
|
5843
7069
|
});
|
|
5844
7070
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
7071
|
+
maybePromote(root, selected.entry.task.id, options);
|
|
5845
7072
|
await claimAndStart(root, selected.entry.task.id, options);
|
|
5846
7073
|
});
|
|
5847
7074
|
const start = program.command("start").description("Start COOP work on a task");
|
|
5848
|
-
start.command("task").description("Start a specific task, or the top ready task if no id is provided").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by track when no id is provided, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery when no id is provided").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid) when no id is provided").option("--today <date>", "Evaluation date (YYYY-MM-DD) when no id is provided").option("--to <assignee>", "Assign the task before starting it").option("--claim", "Assign the task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
|
|
7075
|
+
start.command("task").description("Start a specific task, or the top ready task if no id is provided").argument("[id]", "Task ID or alias").option("--track <track>", "Filter by the home/contributing track lens when no id is provided, else config.defaults.track if set").option("--delivery <delivery>", "Filter by delivery membership when no id is provided").option("--executor <executor>", "Filter by executor (human|ai|ci|hybrid) when no id is provided").option("--today <date>", "Evaluation date (YYYY-MM-DD) when no id is provided").option("--promote", "Promote the task in the current track/version context before starting it").option("--to <assignee>", "Assign the task before starting it").option("--claim", "Assign the task to the actor/user/default author before starting it").option("--actor <actor>", "Actor performing assignment/transition").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").action(async (id, options) => {
|
|
5849
7076
|
const root = resolveRepoRoot();
|
|
7077
|
+
printResolvedSelectionContext(root, options);
|
|
5850
7078
|
const taskId = id?.trim() || selectTopReadyTask(root, {
|
|
5851
7079
|
track: options.track,
|
|
5852
7080
|
delivery: options.delivery,
|
|
7081
|
+
version: options.version,
|
|
5853
7082
|
executor: normalizeExecutor(options.executor),
|
|
5854
7083
|
today: options.today
|
|
5855
7084
|
}).entry.task.id;
|
|
5856
7085
|
const reference = resolveReference(root, taskId, "task");
|
|
5857
|
-
const filePath =
|
|
5858
|
-
const parsed =
|
|
7086
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
7087
|
+
const parsed = parseTaskFile15(filePath);
|
|
5859
7088
|
console.log(
|
|
5860
7089
|
formatSelectedTask(
|
|
5861
7090
|
{
|
|
@@ -5868,29 +7097,31 @@ function registerTaskFlowCommands(program) {
|
|
|
5868
7097
|
{
|
|
5869
7098
|
track: options.track,
|
|
5870
7099
|
delivery: options.delivery,
|
|
7100
|
+
version: options.version,
|
|
5871
7101
|
executor: normalizeExecutor(options.executor),
|
|
5872
7102
|
today: options.today
|
|
5873
7103
|
}
|
|
5874
7104
|
)
|
|
5875
7105
|
);
|
|
7106
|
+
maybePromote(root, reference.id, options);
|
|
5876
7107
|
await claimAndStart(root, reference.id, options);
|
|
5877
7108
|
});
|
|
5878
7109
|
}
|
|
5879
7110
|
|
|
5880
7111
|
// src/commands/ui.ts
|
|
5881
|
-
import
|
|
5882
|
-
import
|
|
7112
|
+
import fs19 from "fs";
|
|
7113
|
+
import path24 from "path";
|
|
5883
7114
|
import { createRequire } from "module";
|
|
5884
7115
|
import { fileURLToPath } from "url";
|
|
5885
7116
|
import { spawn } from "child_process";
|
|
5886
7117
|
import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
|
|
5887
7118
|
function findPackageRoot(entryPath) {
|
|
5888
|
-
let current =
|
|
7119
|
+
let current = path24.dirname(entryPath);
|
|
5889
7120
|
while (true) {
|
|
5890
|
-
if (
|
|
7121
|
+
if (fs19.existsSync(path24.join(current, "package.json"))) {
|
|
5891
7122
|
return current;
|
|
5892
7123
|
}
|
|
5893
|
-
const parent =
|
|
7124
|
+
const parent = path24.dirname(current);
|
|
5894
7125
|
if (parent === current) {
|
|
5895
7126
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
5896
7127
|
}
|
|
@@ -5939,9 +7170,9 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
5939
7170
|
const project = resolveProject(repoRoot);
|
|
5940
7171
|
ensureIndex(repoRoot);
|
|
5941
7172
|
const uiRoot = resolveUiPackageRoot();
|
|
5942
|
-
const requireFromUi = createRequire(
|
|
7173
|
+
const requireFromUi = createRequire(path24.join(uiRoot, "package.json"));
|
|
5943
7174
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
5944
|
-
const viteBin =
|
|
7175
|
+
const viteBin = path24.join(path24.dirname(vitePackageJson), "bin", "vite.js");
|
|
5945
7176
|
const url = `http://${host}:${port}`;
|
|
5946
7177
|
console.log(`COOP UI: ${url}`);
|
|
5947
7178
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -5987,12 +7218,205 @@ function registerUiCommand(program) {
|
|
|
5987
7218
|
});
|
|
5988
7219
|
}
|
|
5989
7220
|
|
|
7221
|
+
// src/commands/update.ts
|
|
7222
|
+
import fs20 from "fs";
|
|
7223
|
+
import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as TaskStatus5, stringifyFrontmatter as stringifyFrontmatter5 } from "@kitsy/coop-core";
|
|
7224
|
+
function collect(value, previous = []) {
|
|
7225
|
+
return [...previous, value];
|
|
7226
|
+
}
|
|
7227
|
+
function unique2(items) {
|
|
7228
|
+
return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
|
|
7229
|
+
}
|
|
7230
|
+
function removeValues(source, values) {
|
|
7231
|
+
if (!source) return source;
|
|
7232
|
+
const removals = new Set((values ?? []).map((value) => value.trim()));
|
|
7233
|
+
const next = source.filter((entry) => !removals.has(entry));
|
|
7234
|
+
return next.length > 0 ? next : void 0;
|
|
7235
|
+
}
|
|
7236
|
+
function addValues(source, values) {
|
|
7237
|
+
const next = unique2([...source ?? [], ...values ?? []]);
|
|
7238
|
+
return next.length > 0 ? next : void 0;
|
|
7239
|
+
}
|
|
7240
|
+
function loadBody(options) {
|
|
7241
|
+
if (options.bodyFile) {
|
|
7242
|
+
return fs20.readFileSync(options.bodyFile, "utf8");
|
|
7243
|
+
}
|
|
7244
|
+
if (options.bodyStdin) {
|
|
7245
|
+
return fs20.readFileSync(0, "utf8");
|
|
7246
|
+
}
|
|
7247
|
+
return void 0;
|
|
7248
|
+
}
|
|
7249
|
+
function applyTrackPriorityOverrides(task, instructions) {
|
|
7250
|
+
if (!instructions || instructions.length === 0) {
|
|
7251
|
+
return task;
|
|
7252
|
+
}
|
|
7253
|
+
const next = { ...task.priority_context ?? {} };
|
|
7254
|
+
for (const instruction of instructions) {
|
|
7255
|
+
const [track, priority] = instruction.split(":", 2).map((part) => part?.trim() ?? "");
|
|
7256
|
+
if (!track || !priority) {
|
|
7257
|
+
throw new Error(`Invalid --priority-in value '${instruction}'. Expected <track>:<priority>.`);
|
|
7258
|
+
}
|
|
7259
|
+
if (!Object.values(TaskPriority3).includes(priority)) {
|
|
7260
|
+
throw new Error(`Invalid priority '${priority}'. Expected one of ${Object.values(TaskPriority3).join(", ")}.`);
|
|
7261
|
+
}
|
|
7262
|
+
next[track] = priority;
|
|
7263
|
+
}
|
|
7264
|
+
return { ...task, priority_context: Object.keys(next).length > 0 ? next : void 0 };
|
|
7265
|
+
}
|
|
7266
|
+
function clearTrackPriorityOverrides(task, tracks) {
|
|
7267
|
+
if (!tracks || tracks.length === 0 || !task.priority_context) {
|
|
7268
|
+
return task;
|
|
7269
|
+
}
|
|
7270
|
+
const next = { ...task.priority_context };
|
|
7271
|
+
for (const track of tracks) {
|
|
7272
|
+
delete next[track];
|
|
7273
|
+
}
|
|
7274
|
+
return { ...task, priority_context: Object.keys(next).length > 0 ? next : void 0 };
|
|
7275
|
+
}
|
|
7276
|
+
function normalizeTaskStatus(status) {
|
|
7277
|
+
const value = status.trim().toLowerCase();
|
|
7278
|
+
if (!Object.values(TaskStatus5).includes(value)) {
|
|
7279
|
+
throw new Error(`Invalid status '${status}'. Expected one of ${Object.values(TaskStatus5).join(", ")}.`);
|
|
7280
|
+
}
|
|
7281
|
+
return value;
|
|
7282
|
+
}
|
|
7283
|
+
function normalizeTaskPriority(priority) {
|
|
7284
|
+
const value = priority.trim().toLowerCase();
|
|
7285
|
+
if (!Object.values(TaskPriority3).includes(value)) {
|
|
7286
|
+
throw new Error(`Invalid priority '${priority}'. Expected one of ${Object.values(TaskPriority3).join(", ")}.`);
|
|
7287
|
+
}
|
|
7288
|
+
return value;
|
|
7289
|
+
}
|
|
7290
|
+
function renderTaskPreview(task, body) {
|
|
7291
|
+
return stringifyFrontmatter5(task, body);
|
|
7292
|
+
}
|
|
7293
|
+
function updateTask(id, options) {
|
|
7294
|
+
const root = resolveRepoRoot();
|
|
7295
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
7296
|
+
let next = {
|
|
7297
|
+
...parsed.task,
|
|
7298
|
+
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
7299
|
+
};
|
|
7300
|
+
if (options.title) next.title = options.title.trim();
|
|
7301
|
+
if (options.priority) next.priority = normalizeTaskPriority(options.priority);
|
|
7302
|
+
if (options.status) next.status = normalizeTaskStatus(options.status);
|
|
7303
|
+
if (options.assign !== void 0) next.assignee = options.assign.trim() || null;
|
|
7304
|
+
if (options.track) next.track = options.track.trim();
|
|
7305
|
+
if (options.delivery !== void 0) next.delivery = options.delivery.trim() || null;
|
|
7306
|
+
if (options.storyPoints !== void 0) next.story_points = Number(options.storyPoints);
|
|
7307
|
+
if (options.plannedHours !== void 0) next = setTaskPlannedHours(next, Number(options.plannedHours));
|
|
7308
|
+
next = {
|
|
7309
|
+
...next,
|
|
7310
|
+
delivery_tracks: addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
|
|
7311
|
+
depends_on: addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
|
|
7312
|
+
tags: addValues(removeValues(next.tags, options.removeTag), options.addTag),
|
|
7313
|
+
fix_versions: addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
|
|
7314
|
+
released_in: addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
|
|
7315
|
+
acceptance: addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
|
|
7316
|
+
tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
|
|
7317
|
+
};
|
|
7318
|
+
next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
|
|
7319
|
+
validateTaskForWrite(next, filePath);
|
|
7320
|
+
const nextBody = loadBody(options) ?? parsed.body;
|
|
7321
|
+
if (options.dryRun) {
|
|
7322
|
+
console.log(renderTaskPreview(next, nextBody).trimEnd());
|
|
7323
|
+
return;
|
|
7324
|
+
}
|
|
7325
|
+
writeTaskEntry(filePath, parsed, next, nextBody);
|
|
7326
|
+
console.log(`Updated ${next.id}`);
|
|
7327
|
+
}
|
|
7328
|
+
function updateIdea(id, options) {
|
|
7329
|
+
const root = resolveRepoRoot();
|
|
7330
|
+
const { filePath, parsed } = loadIdeaEntry(root, id);
|
|
7331
|
+
const nextStatus = options.status?.trim().toLowerCase();
|
|
7332
|
+
if (nextStatus && !Object.values(IdeaStatus3).includes(nextStatus)) {
|
|
7333
|
+
throw new Error(`Invalid idea status '${options.status}'. Expected one of ${Object.values(IdeaStatus3).join(", ")}.`);
|
|
7334
|
+
}
|
|
7335
|
+
const next = {
|
|
7336
|
+
...parsed.idea,
|
|
7337
|
+
title: options.title?.trim() || parsed.idea.title,
|
|
7338
|
+
status: nextStatus || parsed.idea.status,
|
|
7339
|
+
tags: addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag) ?? [],
|
|
7340
|
+
linked_tasks: addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask) ?? []
|
|
7341
|
+
};
|
|
7342
|
+
const nextBody = loadBody(options) ?? parsed.body;
|
|
7343
|
+
if (options.dryRun) {
|
|
7344
|
+
console.log(stringifyFrontmatter5(next, nextBody).trimEnd());
|
|
7345
|
+
return;
|
|
7346
|
+
}
|
|
7347
|
+
writeIdeaFile(filePath, parsed, next, nextBody);
|
|
7348
|
+
console.log(`Updated ${next.id}`);
|
|
7349
|
+
}
|
|
7350
|
+
function registerUpdateCommand(program) {
|
|
7351
|
+
program.command("update").description("Update an existing COOP task or idea").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("[id]", "Entity id when an explicit type is provided").option("--title <title>").option("--priority <priority>").option("--status <status>").option("--assign <user>").option("--track <id>", "Set the task home/origin track").option("--delivery <id>", "Set the task primary delivery id").option("--story-points <n>").option("--planned-hours <n>").option("--add-delivery-track <id>", "", collect, []).option("--remove-delivery-track <id>", "", collect, []).option("--priority-in <track:priority>", "", collect, []).option("--clear-priority-in <track>", "", collect, []).option("--add-dep <id>", "", collect, []).option("--remove-dep <id>", "", collect, []).option("--add-tag <tag>", "", collect, []).option("--remove-tag <tag>", "", collect, []).option("--add-fix-version <v>", "", collect, []).option("--remove-fix-version <v>", "", collect, []).option("--add-released-in <v>", "", collect, []).option("--remove-released-in <v>", "", collect, []).option("--acceptance-add <text>", "", collect, []).option("--acceptance-remove <text>", "", collect, []).option("--tests-add <text>", "", collect, []).option("--tests-remove <text>", "", collect, []).option("--add-linked-task <id>", "", collect, []).option("--remove-linked-task <id>", "", collect, []).option("--body-file <path>").option("--body-stdin").option("--dry-run").action((first, second, options) => {
|
|
7352
|
+
let resolved = resolveOptionalEntityArg(first, second, ["task", "idea"], "task");
|
|
7353
|
+
if (!second && resolved.entity === "task") {
|
|
7354
|
+
try {
|
|
7355
|
+
updateTask(resolved.id, options);
|
|
7356
|
+
return;
|
|
7357
|
+
} catch (error) {
|
|
7358
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7359
|
+
if (!message.includes("not found")) {
|
|
7360
|
+
throw error;
|
|
7361
|
+
}
|
|
7362
|
+
resolved = { entity: "idea", id: first.trim() };
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
if (resolved.entity === "idea") {
|
|
7366
|
+
updateIdea(resolved.id, options);
|
|
7367
|
+
return;
|
|
7368
|
+
}
|
|
7369
|
+
updateTask(resolved.id, options);
|
|
7370
|
+
});
|
|
7371
|
+
}
|
|
7372
|
+
|
|
7373
|
+
// src/commands/use.ts
|
|
7374
|
+
function printContext(values) {
|
|
7375
|
+
const track = values.track?.trim() || "unset";
|
|
7376
|
+
const delivery = values.delivery?.trim() || "unset";
|
|
7377
|
+
const version = values.version?.trim() || "unset";
|
|
7378
|
+
console.log("Working Context:");
|
|
7379
|
+
console.log(`- Track: ${track}`);
|
|
7380
|
+
console.log(`- Delivery: ${delivery}`);
|
|
7381
|
+
console.log(`- Version: ${version}`);
|
|
7382
|
+
if (track === "unset" && delivery === "unset" && version === "unset") {
|
|
7383
|
+
console.log("");
|
|
7384
|
+
console.log("Hint: use `coop use track <id>`, `coop use delivery <id>`, or `coop use version <id>`.");
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
function registerUseCommand(program) {
|
|
7388
|
+
const use = program.command("use").description("Manage user-local working defaults for the current COOP project");
|
|
7389
|
+
use.command("show").description("Show the current working context").action(() => {
|
|
7390
|
+
const root = resolveRepoRoot();
|
|
7391
|
+
printContext(readWorkingContext(root, resolveCoopHome()));
|
|
7392
|
+
});
|
|
7393
|
+
use.command("track").description("Set the default working track").argument("<id>", "Track id").action((id) => {
|
|
7394
|
+
const root = resolveRepoRoot();
|
|
7395
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { track: id }));
|
|
7396
|
+
});
|
|
7397
|
+
use.command("delivery").description("Set the default working delivery").argument("<id>", "Delivery id").action((id) => {
|
|
7398
|
+
const root = resolveRepoRoot();
|
|
7399
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: id }));
|
|
7400
|
+
});
|
|
7401
|
+
use.command("version").description("Set the default working version").argument("<id>", "Version label").action((id) => {
|
|
7402
|
+
const root = resolveRepoRoot();
|
|
7403
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { version: id }));
|
|
7404
|
+
});
|
|
7405
|
+
use.command("clear").description("Clear one working-context key or all of them").argument("[scope]", "track|delivery|version|all", "all").action((scope) => {
|
|
7406
|
+
if (scope !== "track" && scope !== "delivery" && scope !== "version" && scope !== "all") {
|
|
7407
|
+
throw new Error(`Invalid scope '${scope}'. Expected track|delivery|version|all.`);
|
|
7408
|
+
}
|
|
7409
|
+
const root = resolveRepoRoot();
|
|
7410
|
+
printContext(clearWorkingContext(root, resolveCoopHome(), scope));
|
|
7411
|
+
});
|
|
7412
|
+
}
|
|
7413
|
+
|
|
5990
7414
|
// src/commands/view.ts
|
|
5991
7415
|
import {
|
|
5992
7416
|
analyze_feasibility as analyze_feasibility4,
|
|
5993
7417
|
compute_velocity,
|
|
5994
7418
|
load_completed_runs,
|
|
5995
|
-
load_graph as
|
|
7419
|
+
load_graph as load_graph12,
|
|
5996
7420
|
simulate_schedule
|
|
5997
7421
|
} from "@kitsy/coop-core";
|
|
5998
7422
|
var STATUS_COLUMNS = [
|
|
@@ -6055,9 +7479,12 @@ function formatAccuracy(value) {
|
|
|
6055
7479
|
if (value == null) return "-";
|
|
6056
7480
|
return `${Math.round(value * 100)}%`;
|
|
6057
7481
|
}
|
|
7482
|
+
function workedHours2(task) {
|
|
7483
|
+
return (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((sum, entry) => sum + entry.hours, 0);
|
|
7484
|
+
}
|
|
6058
7485
|
function runKanban() {
|
|
6059
7486
|
const root = resolveRepoRoot();
|
|
6060
|
-
const graph =
|
|
7487
|
+
const graph = load_graph12(coopDir(root));
|
|
6061
7488
|
const grouped = /* @__PURE__ */ new Map();
|
|
6062
7489
|
for (const status of STATUS_COLUMNS) {
|
|
6063
7490
|
grouped.set(status, []);
|
|
@@ -6089,11 +7516,19 @@ function runKanban() {
|
|
|
6089
7516
|
}
|
|
6090
7517
|
function runTimeline(options) {
|
|
6091
7518
|
const root = resolveRepoRoot();
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
|
|
7519
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
7520
|
+
const graph = load_graph12(coopDir(root));
|
|
7521
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
7522
|
+
if (isVerboseRequested()) {
|
|
7523
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
7524
|
+
console.log(line);
|
|
7525
|
+
}
|
|
6095
7526
|
}
|
|
6096
|
-
const
|
|
7527
|
+
const deliveryRef = resolvedDelivery.value;
|
|
7528
|
+
if (!deliveryRef) {
|
|
7529
|
+
throw new Error("delivery is not set; pass --delivery <name> or set a default working delivery with `coop use delivery <id>`.");
|
|
7530
|
+
}
|
|
7531
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
6097
7532
|
const include = new Set(delivery.scope.include);
|
|
6098
7533
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
6099
7534
|
const tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -6134,7 +7569,7 @@ function runTimeline(options) {
|
|
|
6134
7569
|
function runVelocity(options) {
|
|
6135
7570
|
const root = resolveRepoRoot();
|
|
6136
7571
|
const coopPath = coopDir(root);
|
|
6137
|
-
const graph =
|
|
7572
|
+
const graph = load_graph12(coopPath);
|
|
6138
7573
|
const config = readCoopConfig(root).raw;
|
|
6139
7574
|
const scheduling = typeof config.scheduling === "object" && config.scheduling !== null ? config.scheduling : {};
|
|
6140
7575
|
const configuredWeeks = Number(scheduling.velocity_window_weeks);
|
|
@@ -6144,12 +7579,19 @@ function runVelocity(options) {
|
|
|
6144
7579
|
today: options.today ?? /* @__PURE__ */ new Date(),
|
|
6145
7580
|
graph
|
|
6146
7581
|
});
|
|
7582
|
+
const completedTaskIds = new Set(runs.map((run) => run.task));
|
|
7583
|
+
const completedTasks = Array.from(completedTaskIds).map((taskId) => graph.nodes.get(taskId)).filter((task) => Boolean(task));
|
|
7584
|
+
const storyPointsTotal = completedTasks.reduce((sum, task) => sum + (task.story_points ?? 0), 0);
|
|
7585
|
+
const plannedHoursTotal = completedTasks.reduce((sum, task) => sum + (task.time?.planned_hours ?? 0), 0);
|
|
7586
|
+
const workedHoursTotal = completedTasks.reduce((sum, task) => sum + workedHours2(task), 0);
|
|
6147
7587
|
console.log(`Velocity: last ${metrics.window_weeks} weeks`);
|
|
6148
7588
|
console.log(`Trend: ${metrics.trend}`);
|
|
6149
7589
|
console.log(`Completed runs: ${metrics.completed_runs}`);
|
|
6150
7590
|
console.log(`Tasks/week: ${metrics.tasks_completed_per_week}`);
|
|
6151
7591
|
console.log(`Hours/week: ${metrics.hours_delivered_per_week}`);
|
|
7592
|
+
console.log(`Story points/week: ${Number((storyPointsTotal / metrics.window_weeks).toFixed(2))}`);
|
|
6152
7593
|
console.log(`Accuracy: ${formatAccuracy(metrics.accuracy_ratio)}`);
|
|
7594
|
+
console.log(`Planned vs Worked: ${plannedHoursTotal.toFixed(2)}h planned | ${workedHoursTotal.toFixed(2)}h worked`);
|
|
6153
7595
|
console.log(`Sparkline: ${sparkline(metrics.points.map((point) => point.completed_tasks))}`);
|
|
6154
7596
|
console.log("");
|
|
6155
7597
|
console.log(
|
|
@@ -6165,12 +7607,20 @@ function runVelocity(options) {
|
|
|
6165
7607
|
}
|
|
6166
7608
|
function runBurndown(options) {
|
|
6167
7609
|
const root = resolveRepoRoot();
|
|
7610
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6168
7611
|
const coopPath = coopDir(root);
|
|
6169
|
-
const graph =
|
|
6170
|
-
|
|
6171
|
-
|
|
7612
|
+
const graph = load_graph12(coopPath);
|
|
7613
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
7614
|
+
if (isVerboseRequested()) {
|
|
7615
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
7616
|
+
console.log(line);
|
|
7617
|
+
}
|
|
7618
|
+
}
|
|
7619
|
+
const deliveryRef = resolvedDelivery.value;
|
|
7620
|
+
if (!deliveryRef) {
|
|
7621
|
+
throw new Error("delivery is not set; pass --delivery <name> or set a default working delivery with `coop use delivery <id>`.");
|
|
6172
7622
|
}
|
|
6173
|
-
const delivery = resolveDelivery(graph,
|
|
7623
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
6174
7624
|
const include = new Set(delivery.scope.include);
|
|
6175
7625
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
6176
7626
|
const scopedTasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -6210,7 +7660,7 @@ function runBurndown(options) {
|
|
|
6210
7660
|
}
|
|
6211
7661
|
function runCapacity(options) {
|
|
6212
7662
|
const root = resolveRepoRoot();
|
|
6213
|
-
const graph =
|
|
7663
|
+
const graph = load_graph12(coopDir(root));
|
|
6214
7664
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
6215
7665
|
if (deliveries.length === 0) {
|
|
6216
7666
|
console.log("No deliveries found.");
|
|
@@ -6298,11 +7748,11 @@ function registerWebhookCommand(program) {
|
|
|
6298
7748
|
}
|
|
6299
7749
|
|
|
6300
7750
|
// src/merge-driver/merge-driver.ts
|
|
6301
|
-
import
|
|
7751
|
+
import fs21 from "fs";
|
|
6302
7752
|
import os2 from "os";
|
|
6303
|
-
import
|
|
7753
|
+
import path25 from "path";
|
|
6304
7754
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6305
|
-
import { stringifyFrontmatter as
|
|
7755
|
+
import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
|
|
6306
7756
|
var STATUS_RANK = {
|
|
6307
7757
|
blocked: 0,
|
|
6308
7758
|
canceled: 0,
|
|
@@ -6359,16 +7809,16 @@ function chooseFieldValue(key, baseValue, oursValue, theirsValue, oursUpdated, t
|
|
|
6359
7809
|
}
|
|
6360
7810
|
function mergeTaskFrontmatter(base, ours, theirs) {
|
|
6361
7811
|
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]);
|
|
6362
|
-
const
|
|
7812
|
+
const output2 = {};
|
|
6363
7813
|
const oursUpdated = asTimestamp(ours.updated);
|
|
6364
7814
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
6365
7815
|
for (const key of keys) {
|
|
6366
7816
|
const merged = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
6367
7817
|
if (merged !== void 0) {
|
|
6368
|
-
|
|
7818
|
+
output2[key] = merged;
|
|
6369
7819
|
}
|
|
6370
7820
|
}
|
|
6371
|
-
return
|
|
7821
|
+
return output2;
|
|
6372
7822
|
}
|
|
6373
7823
|
function mergeTextWithGit(ancestor, ours, theirs) {
|
|
6374
7824
|
const result = spawnSync5("git", ["merge-file", "-p", ours, ancestor, theirs], {
|
|
@@ -6383,33 +7833,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
|
|
|
6383
7833
|
return { ok: false, output: stdout };
|
|
6384
7834
|
}
|
|
6385
7835
|
function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
6386
|
-
const ancestorRaw =
|
|
6387
|
-
const oursRaw =
|
|
6388
|
-
const theirsRaw =
|
|
7836
|
+
const ancestorRaw = fs21.readFileSync(ancestorPath, "utf8");
|
|
7837
|
+
const oursRaw = fs21.readFileSync(oursPath, "utf8");
|
|
7838
|
+
const theirsRaw = fs21.readFileSync(theirsPath, "utf8");
|
|
6389
7839
|
const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
|
|
6390
7840
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
6391
7841
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
6392
7842
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
6393
|
-
const tempDir =
|
|
7843
|
+
const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
|
|
6394
7844
|
try {
|
|
6395
|
-
const ancestorBody =
|
|
6396
|
-
const oursBody =
|
|
6397
|
-
const theirsBody =
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
7845
|
+
const ancestorBody = path25.join(tempDir, "ancestor.md");
|
|
7846
|
+
const oursBody = path25.join(tempDir, "ours.md");
|
|
7847
|
+
const theirsBody = path25.join(tempDir, "theirs.md");
|
|
7848
|
+
fs21.writeFileSync(ancestorBody, ancestor.body, "utf8");
|
|
7849
|
+
fs21.writeFileSync(oursBody, ours.body, "utf8");
|
|
7850
|
+
fs21.writeFileSync(theirsBody, theirs.body, "utf8");
|
|
6401
7851
|
const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
|
|
6402
|
-
const
|
|
6403
|
-
|
|
7852
|
+
const output2 = stringifyFrontmatter6(mergedFrontmatter, mergedBody.output);
|
|
7853
|
+
fs21.writeFileSync(oursPath, output2, "utf8");
|
|
6404
7854
|
return mergedBody.ok ? 0 : 1;
|
|
6405
7855
|
} finally {
|
|
6406
|
-
|
|
7856
|
+
fs21.rmSync(tempDir, { recursive: true, force: true });
|
|
6407
7857
|
}
|
|
6408
7858
|
}
|
|
6409
7859
|
function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
6410
|
-
const ancestor = parseYamlContent3(
|
|
6411
|
-
const ours = parseYamlContent3(
|
|
6412
|
-
const theirs = parseYamlContent3(
|
|
7860
|
+
const ancestor = parseYamlContent3(fs21.readFileSync(ancestorPath, "utf8"), ancestorPath);
|
|
7861
|
+
const ours = parseYamlContent3(fs21.readFileSync(oursPath, "utf8"), oursPath);
|
|
7862
|
+
const theirs = parseYamlContent3(fs21.readFileSync(theirsPath, "utf8"), theirsPath);
|
|
6413
7863
|
const oursUpdated = asTimestamp(ours.updated);
|
|
6414
7864
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
6415
7865
|
const base = ancestor;
|
|
@@ -6419,7 +7869,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
|
6419
7869
|
const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
6420
7870
|
if (value !== void 0) merged[key] = value;
|
|
6421
7871
|
}
|
|
6422
|
-
|
|
7872
|
+
fs21.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
|
|
6423
7873
|
return 0;
|
|
6424
7874
|
}
|
|
6425
7875
|
function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
@@ -6429,12 +7879,38 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
6429
7879
|
return mergeTaskFile(ancestorPath, oursPath, theirsPath);
|
|
6430
7880
|
}
|
|
6431
7881
|
|
|
7882
|
+
// src/utils/basic-help.ts
|
|
7883
|
+
function renderBasicHelp() {
|
|
7884
|
+
return [
|
|
7885
|
+
"COOP Basics",
|
|
7886
|
+
"",
|
|
7887
|
+
"Day-to-day commands:",
|
|
7888
|
+
"- `coop current`: show working context, active work, and the next ready task",
|
|
7889
|
+
"- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
|
|
7890
|
+
"- `coop next task` or `coop pick task`: choose work from COOP",
|
|
7891
|
+
"- `coop show <id>`: inspect a task, idea, or delivery",
|
|
7892
|
+
"- `coop list tasks --track <id>`: browse scoped work",
|
|
7893
|
+
"- `coop update <id> --track <id> --delivery <id>`: update task metadata",
|
|
7894
|
+
'- `coop comment <id> --message "..."`: append a task comment',
|
|
7895
|
+
"- `coop log-time <id> --hours 2 --kind worked`: append time spent",
|
|
7896
|
+
"- `coop review task <id>` / `coop complete task <id>`: move work through lifecycle",
|
|
7897
|
+
"- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`: hand off COOP context to an agent",
|
|
7898
|
+
"",
|
|
7899
|
+
"Use `coop <command> --help` for detailed flags."
|
|
7900
|
+
].join("\n");
|
|
7901
|
+
}
|
|
7902
|
+
|
|
7903
|
+
// src/utils/not-implemented.ts
|
|
7904
|
+
function printNotImplemented(command, phase) {
|
|
7905
|
+
console.log(`${command}: Not yet implemented - coming in Phase ${phase}.`);
|
|
7906
|
+
}
|
|
7907
|
+
|
|
6432
7908
|
// src/index.ts
|
|
6433
7909
|
function readVersion() {
|
|
6434
7910
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
6435
|
-
const packageJsonPath =
|
|
7911
|
+
const packageJsonPath = path26.resolve(path26.dirname(currentFile), "..", "package.json");
|
|
6436
7912
|
try {
|
|
6437
|
-
const parsed = JSON.parse(
|
|
7913
|
+
const parsed = JSON.parse(fs22.readFileSync(packageJsonPath, "utf8"));
|
|
6438
7914
|
return parsed.version ?? "0.0.0";
|
|
6439
7915
|
} catch {
|
|
6440
7916
|
return "0.0.0";
|
|
@@ -6449,14 +7925,24 @@ function createProgram() {
|
|
|
6449
7925
|
const program = new Command();
|
|
6450
7926
|
program.name("coop");
|
|
6451
7927
|
program.version(readVersion());
|
|
6452
|
-
program.description("COOP CLI");
|
|
7928
|
+
program.description("COOP CLI for Git-native planning, backlog management, task execution, and delivery workflows.");
|
|
6453
7929
|
program.option("--verbose", "Print stack traces for command errors");
|
|
6454
7930
|
program.option("-p, --project <id>", "Select the active COOP project");
|
|
7931
|
+
program.addHelpText("after", `
|
|
7932
|
+
Common day-to-day commands:
|
|
7933
|
+
coop basics
|
|
7934
|
+
coop current
|
|
7935
|
+
coop next task
|
|
7936
|
+
coop show <id>
|
|
7937
|
+
`);
|
|
6455
7938
|
registerInitCommand(program);
|
|
6456
7939
|
registerCreateCommand(program);
|
|
7940
|
+
registerCurrentCommand(program);
|
|
6457
7941
|
registerAssignCommand(program);
|
|
7942
|
+
registerCommentCommand(program);
|
|
6458
7943
|
registerAliasCommand(program);
|
|
6459
7944
|
registerConfigCommand(program);
|
|
7945
|
+
registerDepsCommand(program);
|
|
6460
7946
|
registerListCommand(program);
|
|
6461
7947
|
registerShowCommand(program);
|
|
6462
7948
|
registerTransitionCommand(program);
|
|
@@ -6465,19 +7951,28 @@ function createProgram() {
|
|
|
6465
7951
|
registerHelpAiCommand(program);
|
|
6466
7952
|
registerIndexCommand(program);
|
|
6467
7953
|
registerLogCommand(program);
|
|
7954
|
+
registerLogTimeCommand(program);
|
|
6468
7955
|
registerLifecycleCommands(program);
|
|
6469
7956
|
registerMigrateCommand(program);
|
|
6470
7957
|
registerNamingCommand(program);
|
|
6471
7958
|
registerPlanCommand(program);
|
|
7959
|
+
registerPromoteCommand(program);
|
|
6472
7960
|
registerProjectCommand(program);
|
|
7961
|
+
registerPromptCommand(program);
|
|
6473
7962
|
registerRefineCommand(program);
|
|
6474
7963
|
registerRunCommand(program);
|
|
7964
|
+
registerSearchCommand(program);
|
|
6475
7965
|
registerServeCommand(program);
|
|
6476
7966
|
registerStatusCommand(program);
|
|
6477
7967
|
registerUiCommand(program);
|
|
7968
|
+
registerUpdateCommand(program);
|
|
7969
|
+
registerUseCommand(program);
|
|
6478
7970
|
registerViewCommand(program);
|
|
6479
7971
|
registerWebhookCommand(program);
|
|
6480
7972
|
registerPhasePlaceholder(program, "ext", 3, "Plugin extension commands");
|
|
7973
|
+
program.command("basics").alias("help-basic").description("Show the small set of COOP commands that cover most day-to-day work").action(() => {
|
|
7974
|
+
console.log(renderBasicHelp());
|
|
7975
|
+
});
|
|
6481
7976
|
const hooks = program.command("hook");
|
|
6482
7977
|
hooks.command("pre-commit").option("--repo <path>", "Repository root", process.cwd()).action((options) => {
|
|
6483
7978
|
const repoRoot = options.repo ?? process.cwd();
|
|
@@ -6534,7 +8029,7 @@ async function runCli(argv = process.argv) {
|
|
|
6534
8029
|
function isMainModule() {
|
|
6535
8030
|
const entry = process.argv[1];
|
|
6536
8031
|
if (!entry) return false;
|
|
6537
|
-
return
|
|
8032
|
+
return path26.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
6538
8033
|
}
|
|
6539
8034
|
if (isMainModule()) {
|
|
6540
8035
|
await runCli(process.argv);
|