@kitsy/coop 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1378 -371
- 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);
|
|
@@ -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
|
|
@@ -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 output3 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
|
|
1420
|
+
fs3.writeFileSync(filePath, output3, "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, contextValue, sharedDefault2) {
|
|
1578
|
+
if (explicit?.trim()) {
|
|
1579
|
+
return { value: explicit.trim(), source: "arg" };
|
|
1580
|
+
}
|
|
1581
|
+
if (contextValue?.trim()) {
|
|
1582
|
+
return { value: contextValue.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,21 +2124,21 @@ 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
|
|
|
@@ -1907,13 +2157,13 @@ async function ask(question, defaultValue = "") {
|
|
|
1907
2157
|
}
|
|
1908
2158
|
|
|
1909
2159
|
// src/utils/idea-drafts.ts
|
|
1910
|
-
import
|
|
1911
|
-
import
|
|
2160
|
+
import fs5 from "fs";
|
|
2161
|
+
import path6 from "path";
|
|
1912
2162
|
import {
|
|
1913
2163
|
IdeaStatus,
|
|
1914
2164
|
parseFrontmatterContent,
|
|
1915
2165
|
parseYamlContent,
|
|
1916
|
-
stringifyFrontmatter as
|
|
2166
|
+
stringifyFrontmatter as stringifyFrontmatter3
|
|
1917
2167
|
} from "@kitsy/coop-core";
|
|
1918
2168
|
function asUniqueStrings(value) {
|
|
1919
2169
|
if (!Array.isArray(value)) return void 0;
|
|
@@ -1957,7 +2207,7 @@ function parseIdeaDraftInput(content, source) {
|
|
|
1957
2207
|
return parseIdeaDraftObject(parseYamlContent(content, source), source);
|
|
1958
2208
|
}
|
|
1959
2209
|
function writeIdeaFromDraft(root, projectDir, draft) {
|
|
1960
|
-
const existingIds = listIdeaFiles(root).map((filePath2) =>
|
|
2210
|
+
const existingIds = listIdeaFiles(root).map((filePath2) => path6.basename(filePath2, ".md"));
|
|
1961
2211
|
const id = draft.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
1962
2212
|
entityType: "idea",
|
|
1963
2213
|
title: draft.title,
|
|
@@ -1978,25 +2228,25 @@ function writeIdeaFromDraft(root, projectDir, draft) {
|
|
|
1978
2228
|
source: draft.source ?? "manual",
|
|
1979
2229
|
linked_tasks: draft.linked_tasks ?? []
|
|
1980
2230
|
};
|
|
1981
|
-
const filePath =
|
|
1982
|
-
if (
|
|
2231
|
+
const filePath = path6.join(projectDir, "ideas", `${id}.md`);
|
|
2232
|
+
if (fs5.existsSync(filePath)) {
|
|
1983
2233
|
throw new Error(`Idea '${id}' already exists.`);
|
|
1984
2234
|
}
|
|
1985
|
-
|
|
2235
|
+
fs5.writeFileSync(filePath, stringifyFrontmatter3(frontmatter, draft.body ?? ""), "utf8");
|
|
1986
2236
|
return filePath;
|
|
1987
2237
|
}
|
|
1988
2238
|
|
|
1989
2239
|
// src/utils/refinement-drafts.ts
|
|
1990
|
-
import
|
|
1991
|
-
import
|
|
2240
|
+
import fs6 from "fs";
|
|
2241
|
+
import path7 from "path";
|
|
1992
2242
|
import {
|
|
1993
2243
|
IndexManager,
|
|
1994
2244
|
parseFrontmatterContent as parseFrontmatterContent2,
|
|
1995
|
-
parseTaskFile as
|
|
2245
|
+
parseTaskFile as parseTaskFile6,
|
|
1996
2246
|
parseYamlContent as parseYamlContent2,
|
|
1997
2247
|
stringifyYamlContent,
|
|
1998
|
-
validateStructural as
|
|
1999
|
-
writeTask as
|
|
2248
|
+
validateStructural as validateStructural4,
|
|
2249
|
+
writeTask as writeTask5
|
|
2000
2250
|
} from "@kitsy/coop-core";
|
|
2001
2251
|
|
|
2002
2252
|
// src/utils/stdin.ts
|
|
@@ -2023,16 +2273,16 @@ function nonEmptyStrings(value) {
|
|
|
2023
2273
|
return entries.length > 0 ? entries : void 0;
|
|
2024
2274
|
}
|
|
2025
2275
|
function refinementDir(projectDir) {
|
|
2026
|
-
const dir =
|
|
2027
|
-
|
|
2276
|
+
const dir = path7.join(projectDir, "tmp", "refinements");
|
|
2277
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2028
2278
|
return dir;
|
|
2029
2279
|
}
|
|
2030
2280
|
function draftPath(projectDir, mode, sourceId) {
|
|
2031
2281
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2032
|
-
return
|
|
2282
|
+
return path7.join(refinementDir(projectDir), `${mode}-${sourceId}-${stamp}.yml`);
|
|
2033
2283
|
}
|
|
2034
2284
|
function assignCreateProposalIds(root, draft) {
|
|
2035
|
-
const existingIds = listTaskFiles(root).map((filePath) =>
|
|
2285
|
+
const existingIds = listTaskFiles(root).map((filePath) => path7.basename(filePath, ".md"));
|
|
2036
2286
|
const createdIds = [];
|
|
2037
2287
|
const proposals = draft.proposals.map((proposal) => {
|
|
2038
2288
|
if (proposal.action !== "create") {
|
|
@@ -2061,13 +2311,13 @@ function assignCreateProposalIds(root, draft) {
|
|
|
2061
2311
|
};
|
|
2062
2312
|
}
|
|
2063
2313
|
function writeDraftFile(root, projectDir, draft, outputFile) {
|
|
2064
|
-
const filePath = outputFile?.trim() ?
|
|
2065
|
-
|
|
2066
|
-
|
|
2314
|
+
const filePath = outputFile?.trim() ? path7.resolve(root, outputFile.trim()) : draftPath(projectDir, draft.mode, draft.source.id);
|
|
2315
|
+
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
2316
|
+
fs6.writeFileSync(filePath, stringifyYamlContent(draft), "utf8");
|
|
2067
2317
|
return filePath;
|
|
2068
2318
|
}
|
|
2069
2319
|
function printDraftSummary(root, draft, filePath) {
|
|
2070
|
-
console.log(`[COOP] refinement draft created: ${
|
|
2320
|
+
console.log(`[COOP] refinement draft created: ${path7.relative(root, filePath)}`);
|
|
2071
2321
|
console.log(`[COOP] source: ${draft.source.entity_type} ${draft.source.id}`);
|
|
2072
2322
|
console.log(`[COOP] summary: ${draft.summary}`);
|
|
2073
2323
|
for (const proposal of draft.proposals) {
|
|
@@ -2076,7 +2326,7 @@ function printDraftSummary(root, draft, filePath) {
|
|
|
2076
2326
|
`- ${proposal.action.toUpperCase()} ${target ?? "(pending-id)"} | ${proposal.title} | ${proposal.type ?? "feature"} | ${proposal.priority ?? "p2"}`
|
|
2077
2327
|
);
|
|
2078
2328
|
}
|
|
2079
|
-
console.log(`[COOP] apply with: coop apply draft --from-file ${
|
|
2329
|
+
console.log(`[COOP] apply with: coop apply draft --from-file ${path7.relative(root, filePath)}`);
|
|
2080
2330
|
}
|
|
2081
2331
|
function parseRefinementDraftInput(content, source) {
|
|
2082
2332
|
const parsed = parseYamlContent2(content, source);
|
|
@@ -2160,16 +2410,16 @@ function applyCreateProposal(projectDir, proposal) {
|
|
|
2160
2410
|
if (!id) {
|
|
2161
2411
|
throw new Error(`Create proposal '${proposal.title}' is missing id.`);
|
|
2162
2412
|
}
|
|
2163
|
-
const filePath =
|
|
2164
|
-
if (
|
|
2413
|
+
const filePath = path7.join(projectDir, "tasks", `${id}.md`);
|
|
2414
|
+
if (fs6.existsSync(filePath)) {
|
|
2165
2415
|
throw new Error(`Task '${id}' already exists.`);
|
|
2166
2416
|
}
|
|
2167
2417
|
const task = taskFromProposal({ ...proposal, id }, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2168
|
-
const issues =
|
|
2418
|
+
const issues = validateStructural4(task, { filePath });
|
|
2169
2419
|
if (issues.length > 0) {
|
|
2170
2420
|
throw new Error(issues.map((issue) => issue.message).join(" | "));
|
|
2171
2421
|
}
|
|
2172
|
-
|
|
2422
|
+
writeTask5(task, { body: proposal.body ?? "", filePath });
|
|
2173
2423
|
return filePath;
|
|
2174
2424
|
}
|
|
2175
2425
|
function applyUpdateProposal(root, proposal) {
|
|
@@ -2178,7 +2428,7 @@ function applyUpdateProposal(root, proposal) {
|
|
|
2178
2428
|
throw new Error(`Update proposal '${proposal.title}' is missing target_id.`);
|
|
2179
2429
|
}
|
|
2180
2430
|
const filePath = findTaskFileById(root, targetId);
|
|
2181
|
-
const parsed =
|
|
2431
|
+
const parsed = parseTaskFile6(filePath);
|
|
2182
2432
|
const nextTask = {
|
|
2183
2433
|
...parsed.task,
|
|
2184
2434
|
title: proposal.title || parsed.task.title,
|
|
@@ -2196,11 +2446,11 @@ function applyUpdateProposal(root, proposal) {
|
|
|
2196
2446
|
derived_refs: proposal.derived_refs ?? parsed.task.origin?.derived_refs
|
|
2197
2447
|
} : parsed.task.origin
|
|
2198
2448
|
};
|
|
2199
|
-
const issues =
|
|
2449
|
+
const issues = validateStructural4(nextTask, { filePath });
|
|
2200
2450
|
if (issues.length > 0) {
|
|
2201
2451
|
throw new Error(issues.map((issue) => issue.message).join(" | "));
|
|
2202
2452
|
}
|
|
2203
|
-
|
|
2453
|
+
writeTask5(nextTask, {
|
|
2204
2454
|
body: proposal.body ?? parsed.body,
|
|
2205
2455
|
raw: parsed.raw,
|
|
2206
2456
|
filePath
|
|
@@ -2222,8 +2472,8 @@ function applyRefinementDraft(root, projectDir, draft) {
|
|
|
2222
2472
|
}
|
|
2223
2473
|
async function readDraftContent(root, options) {
|
|
2224
2474
|
if (options.fromFile?.trim()) {
|
|
2225
|
-
const filePath =
|
|
2226
|
-
return { content:
|
|
2475
|
+
const filePath = path7.resolve(root, options.fromFile.trim());
|
|
2476
|
+
return { content: fs6.readFileSync(filePath, "utf8"), source: filePath };
|
|
2227
2477
|
}
|
|
2228
2478
|
if (options.stdin) {
|
|
2229
2479
|
return { content: await readStdinText(), source: "<stdin>" };
|
|
@@ -2318,9 +2568,9 @@ function plusDaysIso(days) {
|
|
|
2318
2568
|
function unique(values) {
|
|
2319
2569
|
return Array.from(new Set(values));
|
|
2320
2570
|
}
|
|
2321
|
-
function
|
|
2571
|
+
function resolveIdeaFile2(root, idOrAlias) {
|
|
2322
2572
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
2323
|
-
return
|
|
2573
|
+
return path8.join(root, ...target.file.split("/"));
|
|
2324
2574
|
}
|
|
2325
2575
|
function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
2326
2576
|
const next = unique([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
|
|
@@ -2328,7 +2578,7 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
|
2328
2578
|
...raw,
|
|
2329
2579
|
linked_tasks: next
|
|
2330
2580
|
};
|
|
2331
|
-
|
|
2581
|
+
fs7.writeFileSync(filePath, stringifyFrontmatter4(nextRaw, body), "utf8");
|
|
2332
2582
|
}
|
|
2333
2583
|
function makeTaskDraft(input3) {
|
|
2334
2584
|
return {
|
|
@@ -2365,7 +2615,7 @@ function registerCreateCommand(program) {
|
|
|
2365
2615
|
const written = applyRefinementDraft(root, coop, parsedDraft);
|
|
2366
2616
|
console.log(`[COOP] created ${written.length} task file(s) from ${draftInput.source}`);
|
|
2367
2617
|
for (const filePath of written) {
|
|
2368
|
-
console.log(`Created task: ${
|
|
2618
|
+
console.log(`Created task: ${path8.relative(root, filePath)}`);
|
|
2369
2619
|
}
|
|
2370
2620
|
return;
|
|
2371
2621
|
}
|
|
@@ -2383,8 +2633,8 @@ function registerCreateCommand(program) {
|
|
|
2383
2633
|
let sourceIdeaPath = null;
|
|
2384
2634
|
let sourceIdeaParsed = null;
|
|
2385
2635
|
if (options.from?.trim()) {
|
|
2386
|
-
sourceIdeaPath =
|
|
2387
|
-
sourceIdeaParsed =
|
|
2636
|
+
sourceIdeaPath = resolveIdeaFile2(root, options.from.trim());
|
|
2637
|
+
sourceIdeaParsed = parseIdeaFile3(sourceIdeaPath);
|
|
2388
2638
|
if (options.ai) {
|
|
2389
2639
|
const providerDecomposer = create_provider_idea_decomposer(readCoopConfig(root).raw);
|
|
2390
2640
|
const aiDrafts = await decompose_idea_to_tasks({
|
|
@@ -2456,7 +2706,7 @@ function registerCreateCommand(program) {
|
|
|
2456
2706
|
if (options.id && drafts.length > 1) {
|
|
2457
2707
|
throw new Error("Cannot combine --id with multi-task creation. Remove --id or disable --ai decomposition.");
|
|
2458
2708
|
}
|
|
2459
|
-
const existingIds = listTaskFiles(root).map((filePath) =>
|
|
2709
|
+
const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
|
|
2460
2710
|
const createdIds = [];
|
|
2461
2711
|
for (let index = 0; index < drafts.length; index += 1) {
|
|
2462
2712
|
const draft = drafts[index];
|
|
@@ -2491,19 +2741,19 @@ function registerCreateCommand(program) {
|
|
|
2491
2741
|
promoted_from: options.from?.trim() ? [sourceIdeaParsed?.idea.id ?? options.from.trim()] : void 0
|
|
2492
2742
|
} : void 0
|
|
2493
2743
|
};
|
|
2494
|
-
const filePath =
|
|
2495
|
-
const structuralIssues =
|
|
2744
|
+
const filePath = path8.join(coop, "tasks", `${id}.md`);
|
|
2745
|
+
const structuralIssues = validateStructural5(task, { filePath });
|
|
2496
2746
|
if (structuralIssues.length > 0) {
|
|
2497
2747
|
const message = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
|
|
2498
2748
|
throw new Error(`Task failed structural validation:
|
|
2499
2749
|
${message}`);
|
|
2500
2750
|
}
|
|
2501
|
-
|
|
2751
|
+
writeTask6(task, {
|
|
2502
2752
|
body: draft.body,
|
|
2503
2753
|
filePath
|
|
2504
2754
|
});
|
|
2505
2755
|
createdIds.push(id);
|
|
2506
|
-
console.log(`Created task: ${
|
|
2756
|
+
console.log(`Created task: ${path8.relative(root, filePath)}`);
|
|
2507
2757
|
}
|
|
2508
2758
|
if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
|
|
2509
2759
|
updateIdeaLinkedTasks(
|
|
@@ -2530,7 +2780,7 @@ ${message}`);
|
|
|
2530
2780
|
});
|
|
2531
2781
|
const written = writeIdeaFromDraft(root, coop, parseIdeaDraftInput(draftInput.content, draftInput.source));
|
|
2532
2782
|
console.log(`[COOP] created 1 idea file from ${draftInput.source}`);
|
|
2533
|
-
console.log(`Created idea: ${
|
|
2783
|
+
console.log(`Created idea: ${path8.relative(root, written)}`);
|
|
2534
2784
|
return;
|
|
2535
2785
|
}
|
|
2536
2786
|
const title = options.title?.trim() || titleArg?.trim() || await ask("Idea title");
|
|
@@ -2540,7 +2790,7 @@ ${message}`);
|
|
|
2540
2790
|
const status = (options.status?.trim() || (interactive ? await ask("Idea status", "captured") : "captured")).toLowerCase();
|
|
2541
2791
|
const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
|
|
2542
2792
|
const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
|
|
2543
|
-
const existingIds = listIdeaFiles(root).map((filePath2) =>
|
|
2793
|
+
const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
|
|
2544
2794
|
const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
2545
2795
|
entityType: "idea",
|
|
2546
2796
|
title,
|
|
@@ -2564,9 +2814,9 @@ ${message}`);
|
|
|
2564
2814
|
if (!Object.values(IdeaStatus2).includes(status)) {
|
|
2565
2815
|
throw new Error(`Invalid idea status '${status}'.`);
|
|
2566
2816
|
}
|
|
2567
|
-
const filePath =
|
|
2568
|
-
|
|
2569
|
-
console.log(`Created idea: ${
|
|
2817
|
+
const filePath = path8.join(coop, "ideas", `${id}.md`);
|
|
2818
|
+
fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
|
|
2819
|
+
console.log(`Created idea: ${path8.relative(root, filePath)}`);
|
|
2570
2820
|
});
|
|
2571
2821
|
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
2822
|
const root = resolveRepoRoot();
|
|
@@ -2592,7 +2842,7 @@ ${message}`);
|
|
|
2592
2842
|
throw new Error(`Invalid task type in allowed-types: '${type}'.`);
|
|
2593
2843
|
}
|
|
2594
2844
|
}
|
|
2595
|
-
const existingIds = listTrackFiles(root).map((filePath2) =>
|
|
2845
|
+
const existingIds = listTrackFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml)$/i, ""));
|
|
2596
2846
|
const config = readCoopConfig(root);
|
|
2597
2847
|
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
2598
2848
|
const prefix = typeof idPrefixesRaw.track === "string" ? idPrefixesRaw.track : "TRK";
|
|
@@ -2613,9 +2863,9 @@ ${message}`);
|
|
|
2613
2863
|
allowed_types: allowed
|
|
2614
2864
|
}
|
|
2615
2865
|
};
|
|
2616
|
-
const filePath =
|
|
2866
|
+
const filePath = path8.join(coop, "tracks", `${id}.yml`);
|
|
2617
2867
|
writeYamlFile3(filePath, payload);
|
|
2618
|
-
console.log(`Created track: ${
|
|
2868
|
+
console.log(`Created track: ${path8.relative(root, filePath)}`);
|
|
2619
2869
|
});
|
|
2620
2870
|
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
2871
|
const root = resolveRepoRoot();
|
|
@@ -2668,7 +2918,7 @@ ${message}`);
|
|
|
2668
2918
|
options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
|
|
2669
2919
|
);
|
|
2670
2920
|
const tasks = listTaskFiles(root).map((filePath2) => {
|
|
2671
|
-
const parsed =
|
|
2921
|
+
const parsed = parseTaskFile7(filePath2).task;
|
|
2672
2922
|
return { id: parsed.id, title: parsed.title };
|
|
2673
2923
|
});
|
|
2674
2924
|
if (interactive && tasks.length > 0) {
|
|
@@ -2697,7 +2947,7 @@ ${message}`);
|
|
|
2697
2947
|
}
|
|
2698
2948
|
}
|
|
2699
2949
|
const existingIds = listDeliveryFiles(root).map(
|
|
2700
|
-
(filePath2) =>
|
|
2950
|
+
(filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, "")
|
|
2701
2951
|
);
|
|
2702
2952
|
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
2703
2953
|
const prefix = typeof idPrefixesRaw.delivery === "string" ? idPrefixesRaw.delivery : "DEL";
|
|
@@ -2726,9 +2976,66 @@ ${message}`);
|
|
|
2726
2976
|
exclude: scopeExclude
|
|
2727
2977
|
}
|
|
2728
2978
|
};
|
|
2729
|
-
const filePath =
|
|
2979
|
+
const filePath = path8.join(coop, "deliveries", `${id}.yml`);
|
|
2730
2980
|
writeYamlFile3(filePath, payload);
|
|
2731
|
-
console.log(`Created delivery: ${
|
|
2981
|
+
console.log(`Created delivery: ${path8.relative(root, filePath)}`);
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// src/commands/current.ts
|
|
2986
|
+
function registerCurrentCommand(program) {
|
|
2987
|
+
program.command("current").description("Show active project, working context, my in-progress tasks, and the next ready task").action(() => {
|
|
2988
|
+
const root = resolveRepoRoot();
|
|
2989
|
+
const identity = readCoopIdentity(root);
|
|
2990
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
2991
|
+
const actor = defaultCoopAuthor(root);
|
|
2992
|
+
const inProgress = loadTasks(root).filter(
|
|
2993
|
+
(task) => task.assignee === actor && (task.status === "in_progress" || task.status === "in_review")
|
|
2994
|
+
);
|
|
2995
|
+
console.log(`Project: ${identity.name} (${identity.id})`);
|
|
2996
|
+
console.log(`Actor: ${actor}`);
|
|
2997
|
+
console.log(`Track: ${context.track ?? "-"}`);
|
|
2998
|
+
console.log(`Delivery: ${context.delivery ?? "-"}`);
|
|
2999
|
+
console.log(`Version: ${context.version ?? "-"}`);
|
|
3000
|
+
console.log("");
|
|
3001
|
+
console.log("My Active Tasks:");
|
|
3002
|
+
if (inProgress.length === 0) {
|
|
3003
|
+
console.log("- none");
|
|
3004
|
+
} else {
|
|
3005
|
+
for (const task of inProgress) {
|
|
3006
|
+
console.log(`- ${task.id} [${task.status}] ${task.title}`);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
console.log("");
|
|
3010
|
+
console.log("Next Ready:");
|
|
3011
|
+
try {
|
|
3012
|
+
const selected = selectTopReadyTask(root, {
|
|
3013
|
+
track: context.track,
|
|
3014
|
+
delivery: context.delivery,
|
|
3015
|
+
version: context.version
|
|
3016
|
+
});
|
|
3017
|
+
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// src/commands/deps.ts
|
|
3025
|
+
import { load_graph as load_graph3 } from "@kitsy/coop-core";
|
|
3026
|
+
function registerDepsCommand(program) {
|
|
3027
|
+
program.command("deps").description("Show dependencies and reverse dependencies for a task").argument("<id>", "Task id or alias").action((id) => {
|
|
3028
|
+
const root = resolveRepoRoot();
|
|
3029
|
+
const graph = load_graph3(coopDir(root));
|
|
3030
|
+
const reference = resolveReference(root, id, "task");
|
|
3031
|
+
const task = graph.nodes.get(reference.id);
|
|
3032
|
+
if (!task) {
|
|
3033
|
+
throw new Error(`Task '${reference.id}' not found.`);
|
|
3034
|
+
}
|
|
3035
|
+
const reverse = Array.from(graph.reverse.get(task.id) ?? []).sort((a, b) => a.localeCompare(b));
|
|
3036
|
+
console.log(`Task: ${task.id}`);
|
|
3037
|
+
console.log(`Depends On: ${task.depends_on && task.depends_on.length > 0 ? task.depends_on.join(", ") : "-"}`);
|
|
3038
|
+
console.log(`Required By: ${reverse.length > 0 ? reverse.join(", ") : "-"}`);
|
|
2732
3039
|
});
|
|
2733
3040
|
}
|
|
2734
3041
|
|
|
@@ -2737,7 +3044,8 @@ import chalk from "chalk";
|
|
|
2737
3044
|
import {
|
|
2738
3045
|
compute_critical_path,
|
|
2739
3046
|
compute_readiness_with_corrections,
|
|
2740
|
-
|
|
3047
|
+
effective_priority as effective_priority2,
|
|
3048
|
+
load_graph as load_graph4,
|
|
2741
3049
|
schedule_next as schedule_next2,
|
|
2742
3050
|
topological_sort,
|
|
2743
3051
|
validate_graph
|
|
@@ -2790,7 +3098,7 @@ function renderAsciiDag(tasks, order) {
|
|
|
2790
3098
|
}
|
|
2791
3099
|
function runValidate() {
|
|
2792
3100
|
const root = resolveRepoRoot();
|
|
2793
|
-
const graph =
|
|
3101
|
+
const graph = load_graph4(coopDir(root));
|
|
2794
3102
|
const issues = validate_graph(graph);
|
|
2795
3103
|
if (issues.length === 0) {
|
|
2796
3104
|
console.log(chalk.green("Graph is healthy. No invariant violations found."));
|
|
@@ -2805,12 +3113,20 @@ function runValidate() {
|
|
|
2805
3113
|
}
|
|
2806
3114
|
function runNext(options) {
|
|
2807
3115
|
const root = resolveRepoRoot();
|
|
2808
|
-
const
|
|
3116
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
3117
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track"));
|
|
3118
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
3119
|
+
if (isVerboseRequested()) {
|
|
3120
|
+
for (const line of formatResolvedContextMessage({ track: resolvedTrack, delivery: resolvedDelivery })) {
|
|
3121
|
+
console.log(line);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const graph = load_graph4(coopDir(root));
|
|
2809
3125
|
const readiness = compute_readiness_with_corrections(graph);
|
|
2810
3126
|
const limit = options.limit && options.limit.trim().length > 0 ? Number(options.limit) : void 0;
|
|
2811
3127
|
const ready = schedule_next2(graph, {
|
|
2812
|
-
track:
|
|
2813
|
-
delivery:
|
|
3128
|
+
track: resolvedTrack.value,
|
|
3129
|
+
delivery: resolvedDelivery.value,
|
|
2814
3130
|
executor: options.executor,
|
|
2815
3131
|
today: options.today,
|
|
2816
3132
|
limit: Number.isInteger(limit) && Number(limit) > 0 ? Number(limit) : void 0
|
|
@@ -2824,7 +3140,7 @@ function runNext(options) {
|
|
|
2824
3140
|
ready.map((entry) => [
|
|
2825
3141
|
entry.task.id,
|
|
2826
3142
|
entry.task.title,
|
|
2827
|
-
entry.task.
|
|
3143
|
+
effective_priority2(entry.task, resolvedTrack.value),
|
|
2828
3144
|
entry.task.track ?? "-",
|
|
2829
3145
|
entry.score.toFixed(1),
|
|
2830
3146
|
entry.fits_capacity ? chalk.green("yes") : chalk.yellow("no"),
|
|
@@ -2854,13 +3170,13 @@ Warnings (${readiness.warnings.length}):`);
|
|
|
2854
3170
|
}
|
|
2855
3171
|
function runShow() {
|
|
2856
3172
|
const root = resolveRepoRoot();
|
|
2857
|
-
const graph =
|
|
3173
|
+
const graph = load_graph4(coopDir(root));
|
|
2858
3174
|
const order = topological_sort(graph);
|
|
2859
3175
|
console.log(renderAsciiDag(graph.nodes, order));
|
|
2860
3176
|
}
|
|
2861
3177
|
function runCriticalPath(deliveryName) {
|
|
2862
3178
|
const root = resolveRepoRoot();
|
|
2863
|
-
const graph =
|
|
3179
|
+
const graph = load_graph4(coopDir(root));
|
|
2864
3180
|
const delivery = resolveDelivery(graph, deliveryName);
|
|
2865
3181
|
const result = compute_critical_path(delivery, graph);
|
|
2866
3182
|
console.log(`Critical Path: ${delivery.name} (${delivery.id})`);
|
|
@@ -2899,13 +3215,15 @@ function registerGraphCommand(program) {
|
|
|
2899
3215
|
}
|
|
2900
3216
|
|
|
2901
3217
|
// src/utils/ai-help.ts
|
|
2902
|
-
import
|
|
3218
|
+
import path9 from "path";
|
|
2903
3219
|
var catalog = {
|
|
2904
3220
|
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
3221
|
selection_rules: [
|
|
2906
3222
|
"Use `coop project show` first to confirm the active workspace and project.",
|
|
3223
|
+
"Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
|
|
2907
3224
|
"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
|
-
"
|
|
3225
|
+
"Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
|
|
3226
|
+
"Use `coop show <id>` or `coop show task <id>` before implementation to read acceptance, tests_required, dependencies, origin refs, and task metadata.",
|
|
2909
3227
|
"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
3228
|
],
|
|
2911
3229
|
workspace_rules: [
|
|
@@ -2955,6 +3273,11 @@ var catalog = {
|
|
|
2955
3273
|
{ usage: "coop project list", purpose: "List projects in the current workspace." },
|
|
2956
3274
|
{ usage: "coop project show", purpose: "Show the active project id, name, path, and layout." },
|
|
2957
3275
|
{ usage: "coop project use <id>", purpose: "Switch the active project in a multi-project workspace." },
|
|
3276
|
+
{ usage: "coop use show", purpose: "Show the user-local working defaults for track, delivery, and version." },
|
|
3277
|
+
{ usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
|
|
3278
|
+
{ usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
|
|
3279
|
+
{ usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
|
|
3280
|
+
{ usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
|
|
2958
3281
|
{ usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
|
|
2959
3282
|
{ usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
|
|
2960
3283
|
]
|
|
@@ -2993,8 +3316,10 @@ var catalog = {
|
|
|
2993
3316
|
commands: [
|
|
2994
3317
|
{ usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
|
|
2995
3318
|
{ usage: "coop graph next --delivery MVP", purpose: "Show the ready queue for a delivery with scores and blockers." },
|
|
3319
|
+
{ 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
3320
|
{ 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." },
|
|
3321
|
+
{ 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." },
|
|
3322
|
+
{ usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context." },
|
|
2998
3323
|
{ usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
|
|
2999
3324
|
{ usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
|
|
3000
3325
|
{ usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
|
|
@@ -3008,8 +3333,18 @@ var catalog = {
|
|
|
3008
3333
|
description: "Read backlog state, task details, and planning output.",
|
|
3009
3334
|
commands: [
|
|
3010
3335
|
{ usage: "coop list tasks --status todo", purpose: "List tasks with filters." },
|
|
3336
|
+
{ usage: "coop list tasks --track MVP --delivery MVP --ready", purpose: "List ready tasks with track and delivery filtering." },
|
|
3337
|
+
{ usage: "coop list tasks --mine", purpose: "List tasks assigned to the current default COOP author." },
|
|
3338
|
+
{ usage: 'coop search "auth and login form"', purpose: "Run deterministic non-AI search across tasks, ideas, and deliveries." },
|
|
3339
|
+
{ usage: 'coop search "auth" --open', purpose: "Require a single match and print the resolved summary row." },
|
|
3340
|
+
{ usage: "coop show PM-101", purpose: "Resolve a task, idea, or delivery by reference without an extra entity noun." },
|
|
3011
3341
|
{ usage: "coop show task PM-101", purpose: "Show a task with acceptance, tests_required, refs, and runbook sections." },
|
|
3012
3342
|
{ usage: "coop show idea IDEA-101", purpose: "Show an idea." },
|
|
3343
|
+
{ usage: "coop deps PM-101", purpose: "Show task dependencies and reverse dependencies." },
|
|
3344
|
+
{ usage: "coop prompt PM-101 --format markdown", purpose: "Generate a manual handoff prompt from a task and current working context." },
|
|
3345
|
+
{ usage: "coop update PM-101 --priority p1 --add-fix-version v2", purpose: "Update task metadata without editing `.coop` files directly." },
|
|
3346
|
+
{ usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
|
|
3347
|
+
{ usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
|
|
3013
3348
|
{ usage: "coop plan delivery MVP", purpose: "Run delivery feasibility analysis." },
|
|
3014
3349
|
{ usage: "coop plan delivery MVP --monte-carlo --iterations 5000", purpose: "Run probabilistic delivery forecasting." },
|
|
3015
3350
|
{ usage: "coop view velocity", purpose: "Show historical throughput." },
|
|
@@ -3052,6 +3387,7 @@ var catalog = {
|
|
|
3052
3387
|
execution_model: [
|
|
3053
3388
|
"Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
|
|
3054
3389
|
"Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
|
|
3390
|
+
"Use `coop update`, `coop comment`, and `coop log-time` for task mutations instead of manually editing task files.",
|
|
3055
3391
|
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
|
|
3056
3392
|
"If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
|
|
3057
3393
|
],
|
|
@@ -3168,7 +3504,7 @@ function renderAiHelpTopic(format, topic) {
|
|
|
3168
3504
|
`;
|
|
3169
3505
|
}
|
|
3170
3506
|
function normalizeRepoPath(repoPath) {
|
|
3171
|
-
return repoPath ?
|
|
3507
|
+
return repoPath ? path9.resolve(repoPath) : process.cwd();
|
|
3172
3508
|
}
|
|
3173
3509
|
function formatSelectionCommand(commandName, delivery, track) {
|
|
3174
3510
|
if (delivery) {
|
|
@@ -3209,12 +3545,15 @@ function renderInitialPrompt(options = {}) {
|
|
|
3209
3545
|
`- If you are unsure about lifecycle changes, run \`${commandName} help-ai --state-transitions --format markdown\`.`,
|
|
3210
3546
|
`- If you are unsure where artifacts should go, run \`${commandName} help-ai --artifacts --format markdown\`.`,
|
|
3211
3547
|
`- If you are unsure whether to continue after a task, run \`${commandName} help-ai --post-execution --format markdown\`.`,
|
|
3548
|
+
`- Use \`${commandName} use show\` to inspect current working track, delivery, and version defaults before selecting work.`,
|
|
3212
3549
|
"- Use only commands that actually exist in COOP. Do not invent command names.",
|
|
3213
3550
|
"- Do not reprioritize work outside COOP unless the user explicitly overrides it.",
|
|
3214
3551
|
"- Select only the first ready task from the COOP readiness output.",
|
|
3215
|
-
"- Inspect the selected task with `coop show
|
|
3552
|
+
"- Inspect the selected task with `coop show <id>` before implementation.",
|
|
3216
3553
|
"- 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
3554
|
"- Respect lifecycle prerequisites: use `coop review task <id>` before `coop complete task <id>`; do not complete directly from `in_progress`.",
|
|
3555
|
+
"- Use `coop promote <id>` or `coop pick/start --promote` when the selected task must be escalated inside the active track/version context.",
|
|
3556
|
+
"- Use `coop update <id>`, `coop comment <id>`, and `coop log-time <id>` for task metadata changes; do not hand-edit task frontmatter.",
|
|
3218
3557
|
`- Write any contract-review, audit, or planning artifact under \`${artifactsDir}\` unless the user explicitly chooses another location.`
|
|
3219
3558
|
];
|
|
3220
3559
|
if (rigour === "strict") {
|
|
@@ -3372,11 +3711,11 @@ function resolveHelpTopic(options) {
|
|
|
3372
3711
|
if (options.naming) {
|
|
3373
3712
|
requestedTopics.push("naming");
|
|
3374
3713
|
}
|
|
3375
|
-
const
|
|
3376
|
-
if (
|
|
3377
|
-
throw new Error(`Specify only one focused help-ai topic at a time. Received: ${
|
|
3714
|
+
const unique3 = [...new Set(requestedTopics)];
|
|
3715
|
+
if (unique3.length > 1) {
|
|
3716
|
+
throw new Error(`Specify only one focused help-ai topic at a time. Received: ${unique3.join(", ")}.`);
|
|
3378
3717
|
}
|
|
3379
|
-
const topic =
|
|
3718
|
+
const topic = unique3[0];
|
|
3380
3719
|
if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
|
|
3381
3720
|
throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
|
|
3382
3721
|
}
|
|
@@ -3384,7 +3723,7 @@ function resolveHelpTopic(options) {
|
|
|
3384
3723
|
}
|
|
3385
3724
|
|
|
3386
3725
|
// src/commands/index.ts
|
|
3387
|
-
import
|
|
3726
|
+
import path10 from "path";
|
|
3388
3727
|
import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
|
|
3389
3728
|
function runStatus(options) {
|
|
3390
3729
|
const root = resolveRepoRoot();
|
|
@@ -3394,7 +3733,7 @@ function runStatus(options) {
|
|
|
3394
3733
|
const freshness = status.stale ? "stale" : "fresh";
|
|
3395
3734
|
const existsText = status.exists ? "present" : "missing";
|
|
3396
3735
|
console.log(`[COOP] index ${existsText}, ${freshness}`);
|
|
3397
|
-
console.log(`[COOP] graph: ${
|
|
3736
|
+
console.log(`[COOP] graph: ${path10.relative(root, status.graph_path)}`);
|
|
3398
3737
|
if (status.generated_at) {
|
|
3399
3738
|
console.log(`[COOP] generated_at: ${status.generated_at}`);
|
|
3400
3739
|
}
|
|
@@ -3417,7 +3756,7 @@ function runRebuild() {
|
|
|
3417
3756
|
const graph = manager.build_full_index();
|
|
3418
3757
|
const elapsed = Date.now() - start;
|
|
3419
3758
|
console.log(`[COOP] index rebuilt: ${graph.nodes.size} tasks (${elapsed} ms)`);
|
|
3420
|
-
console.log(`[COOP] graph: ${
|
|
3759
|
+
console.log(`[COOP] graph: ${path10.relative(root, manager.graphPath)}`);
|
|
3421
3760
|
}
|
|
3422
3761
|
function registerIndexCommand(program) {
|
|
3423
3762
|
const index = program.command("index").description("Index management commands");
|
|
@@ -3433,18 +3772,18 @@ function registerIndexCommand(program) {
|
|
|
3433
3772
|
}
|
|
3434
3773
|
|
|
3435
3774
|
// src/commands/init.ts
|
|
3436
|
-
import
|
|
3437
|
-
import
|
|
3775
|
+
import fs10 from "fs";
|
|
3776
|
+
import path13 from "path";
|
|
3438
3777
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3439
3778
|
import { createInterface } from "readline/promises";
|
|
3440
3779
|
import { stdin as input, stdout as output } from "process";
|
|
3441
3780
|
import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
3442
3781
|
|
|
3443
3782
|
// src/hooks/pre-commit.ts
|
|
3444
|
-
import
|
|
3445
|
-
import
|
|
3783
|
+
import fs8 from "fs";
|
|
3784
|
+
import path11 from "path";
|
|
3446
3785
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3447
|
-
import { detect_cycle, parseTaskContent, parseTaskFile as
|
|
3786
|
+
import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural6 } from "@kitsy/coop-core";
|
|
3448
3787
|
var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
|
|
3449
3788
|
var HOOK_BLOCK_END = "# COOP_PRE_COMMIT_END";
|
|
3450
3789
|
function runGit(repoRoot, args, allowFailure = false) {
|
|
@@ -3473,28 +3812,28 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
|
|
|
3473
3812
|
const normalized = toPosixPath2(relativePath);
|
|
3474
3813
|
const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
|
|
3475
3814
|
if (projectMatch?.[1]) {
|
|
3476
|
-
return
|
|
3815
|
+
return path11.join(repoRoot, ".coop", "projects", projectMatch[1]);
|
|
3477
3816
|
}
|
|
3478
3817
|
if (normalized.startsWith(".coop/tasks/")) {
|
|
3479
|
-
return
|
|
3818
|
+
return path11.join(repoRoot, ".coop");
|
|
3480
3819
|
}
|
|
3481
3820
|
throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
|
|
3482
3821
|
}
|
|
3483
3822
|
function listTaskFilesForProject(projectRoot) {
|
|
3484
|
-
const tasksDir =
|
|
3485
|
-
if (!
|
|
3823
|
+
const tasksDir = path11.join(projectRoot, "tasks");
|
|
3824
|
+
if (!fs8.existsSync(tasksDir)) return [];
|
|
3486
3825
|
const out = [];
|
|
3487
3826
|
const stack = [tasksDir];
|
|
3488
3827
|
while (stack.length > 0) {
|
|
3489
3828
|
const current = stack.pop();
|
|
3490
|
-
const entries =
|
|
3829
|
+
const entries = fs8.readdirSync(current, { withFileTypes: true });
|
|
3491
3830
|
for (const entry of entries) {
|
|
3492
|
-
const fullPath =
|
|
3831
|
+
const fullPath = path11.join(current, entry.name);
|
|
3493
3832
|
if (entry.isDirectory()) {
|
|
3494
3833
|
stack.push(fullPath);
|
|
3495
3834
|
continue;
|
|
3496
3835
|
}
|
|
3497
|
-
if (entry.isFile() &&
|
|
3836
|
+
if (entry.isFile() && path11.extname(entry.name).toLowerCase() === ".md") {
|
|
3498
3837
|
out.push(fullPath);
|
|
3499
3838
|
}
|
|
3500
3839
|
}
|
|
@@ -3513,7 +3852,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3513
3852
|
const errors = [];
|
|
3514
3853
|
const staged = [];
|
|
3515
3854
|
for (const relativePath of relativePaths) {
|
|
3516
|
-
const absolutePath =
|
|
3855
|
+
const absolutePath = path11.join(repoRoot, ...relativePath.split("/"));
|
|
3517
3856
|
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
3518
3857
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
3519
3858
|
if (!stagedBlob) {
|
|
@@ -3528,7 +3867,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3528
3867
|
errors.push(`[COOP] ${message}`);
|
|
3529
3868
|
continue;
|
|
3530
3869
|
}
|
|
3531
|
-
const issues =
|
|
3870
|
+
const issues = validateStructural6(task, { filePath: absolutePath });
|
|
3532
3871
|
for (const issue of issues) {
|
|
3533
3872
|
errors.push(`[COOP] ${relativePath}: ${issue.message}`);
|
|
3534
3873
|
}
|
|
@@ -3577,13 +3916,13 @@ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
|
3577
3916
|
}
|
|
3578
3917
|
const tasks = [];
|
|
3579
3918
|
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
3580
|
-
const normalized = toPosixPath2(
|
|
3919
|
+
const normalized = toPosixPath2(path11.resolve(filePath));
|
|
3581
3920
|
const stagedTask = stagedByPath.get(normalized);
|
|
3582
3921
|
if (stagedTask) {
|
|
3583
3922
|
tasks.push(stagedTask);
|
|
3584
3923
|
continue;
|
|
3585
3924
|
}
|
|
3586
|
-
tasks.push(
|
|
3925
|
+
tasks.push(parseTaskFile8(filePath).task);
|
|
3587
3926
|
}
|
|
3588
3927
|
return tasks;
|
|
3589
3928
|
}
|
|
@@ -3607,7 +3946,7 @@ function runPreCommitChecks(repoRoot) {
|
|
|
3607
3946
|
const graph = buildGraphForCycleCheck(tasks);
|
|
3608
3947
|
const cycle = detect_cycle(graph);
|
|
3609
3948
|
if (cycle) {
|
|
3610
|
-
const projectLabel = toPosixPath2(
|
|
3949
|
+
const projectLabel = toPosixPath2(path11.relative(repoRoot, projectRoot));
|
|
3611
3950
|
errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
|
|
3612
3951
|
}
|
|
3613
3952
|
} catch (error) {
|
|
@@ -3639,9 +3978,9 @@ function hookScriptBlock() {
|
|
|
3639
3978
|
].join("\n");
|
|
3640
3979
|
}
|
|
3641
3980
|
function installPreCommitHook(repoRoot) {
|
|
3642
|
-
const hookPath =
|
|
3643
|
-
const hookDir =
|
|
3644
|
-
if (!
|
|
3981
|
+
const hookPath = path11.join(repoRoot, ".git", "hooks", "pre-commit");
|
|
3982
|
+
const hookDir = path11.dirname(hookPath);
|
|
3983
|
+
if (!fs8.existsSync(hookDir)) {
|
|
3645
3984
|
return {
|
|
3646
3985
|
installed: false,
|
|
3647
3986
|
hookPath,
|
|
@@ -3649,18 +3988,18 @@ function installPreCommitHook(repoRoot) {
|
|
|
3649
3988
|
};
|
|
3650
3989
|
}
|
|
3651
3990
|
const block = hookScriptBlock();
|
|
3652
|
-
if (!
|
|
3991
|
+
if (!fs8.existsSync(hookPath)) {
|
|
3653
3992
|
const content = ["#!/bin/sh", "", block].join("\n");
|
|
3654
|
-
|
|
3993
|
+
fs8.writeFileSync(hookPath, content, "utf8");
|
|
3655
3994
|
} else {
|
|
3656
|
-
const existing =
|
|
3995
|
+
const existing = fs8.readFileSync(hookPath, "utf8");
|
|
3657
3996
|
if (!existing.includes(HOOK_BLOCK_START)) {
|
|
3658
3997
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3659
|
-
|
|
3998
|
+
fs8.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3660
3999
|
}
|
|
3661
4000
|
}
|
|
3662
4001
|
try {
|
|
3663
|
-
|
|
4002
|
+
fs8.chmodSync(hookPath, 493);
|
|
3664
4003
|
} catch {
|
|
3665
4004
|
}
|
|
3666
4005
|
return {
|
|
@@ -3671,15 +4010,15 @@ function installPreCommitHook(repoRoot) {
|
|
|
3671
4010
|
}
|
|
3672
4011
|
|
|
3673
4012
|
// src/hooks/post-merge-validate.ts
|
|
3674
|
-
import
|
|
3675
|
-
import
|
|
4013
|
+
import fs9 from "fs";
|
|
4014
|
+
import path12 from "path";
|
|
3676
4015
|
import { list_projects } from "@kitsy/coop-core";
|
|
3677
|
-
import { load_graph as
|
|
4016
|
+
import { load_graph as load_graph5, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
3678
4017
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
3679
4018
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
3680
4019
|
function runPostMergeValidate(repoRoot) {
|
|
3681
|
-
const workspaceDir =
|
|
3682
|
-
if (!
|
|
4020
|
+
const workspaceDir = path12.join(repoRoot, ".coop");
|
|
4021
|
+
if (!fs9.existsSync(workspaceDir)) {
|
|
3683
4022
|
return {
|
|
3684
4023
|
ok: true,
|
|
3685
4024
|
warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
|
|
@@ -3695,7 +4034,7 @@ function runPostMergeValidate(repoRoot) {
|
|
|
3695
4034
|
}
|
|
3696
4035
|
const warnings = [];
|
|
3697
4036
|
for (const project of projects) {
|
|
3698
|
-
const graph =
|
|
4037
|
+
const graph = load_graph5(project.root);
|
|
3699
4038
|
const issues = validate_graph2(graph);
|
|
3700
4039
|
for (const issue of issues) {
|
|
3701
4040
|
warnings.push(`[COOP] post-merge warning [${project.id}] [${issue.invariant}] ${issue.message}`);
|
|
@@ -3731,9 +4070,9 @@ function postMergeHookBlock() {
|
|
|
3731
4070
|
].join("\n");
|
|
3732
4071
|
}
|
|
3733
4072
|
function installPostMergeHook(repoRoot) {
|
|
3734
|
-
const hookPath =
|
|
3735
|
-
const hookDir =
|
|
3736
|
-
if (!
|
|
4073
|
+
const hookPath = path12.join(repoRoot, ".git", "hooks", "post-merge");
|
|
4074
|
+
const hookDir = path12.dirname(hookPath);
|
|
4075
|
+
if (!fs9.existsSync(hookDir)) {
|
|
3737
4076
|
return {
|
|
3738
4077
|
installed: false,
|
|
3739
4078
|
hookPath,
|
|
@@ -3741,17 +4080,17 @@ function installPostMergeHook(repoRoot) {
|
|
|
3741
4080
|
};
|
|
3742
4081
|
}
|
|
3743
4082
|
const block = postMergeHookBlock();
|
|
3744
|
-
if (!
|
|
3745
|
-
|
|
4083
|
+
if (!fs9.existsSync(hookPath)) {
|
|
4084
|
+
fs9.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
|
|
3746
4085
|
} else {
|
|
3747
|
-
const existing =
|
|
4086
|
+
const existing = fs9.readFileSync(hookPath, "utf8");
|
|
3748
4087
|
if (!existing.includes(HOOK_BLOCK_START2)) {
|
|
3749
4088
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3750
|
-
|
|
4089
|
+
fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3751
4090
|
}
|
|
3752
4091
|
}
|
|
3753
4092
|
try {
|
|
3754
|
-
|
|
4093
|
+
fs9.chmodSync(hookPath, 493);
|
|
3755
4094
|
} catch {
|
|
3756
4095
|
}
|
|
3757
4096
|
return {
|
|
@@ -3917,40 +4256,40 @@ tmp/
|
|
|
3917
4256
|
*.tmp
|
|
3918
4257
|
`;
|
|
3919
4258
|
function ensureDir(dirPath) {
|
|
3920
|
-
|
|
4259
|
+
fs10.mkdirSync(dirPath, { recursive: true });
|
|
3921
4260
|
}
|
|
3922
4261
|
function writeIfMissing(filePath, content) {
|
|
3923
|
-
if (!
|
|
3924
|
-
|
|
4262
|
+
if (!fs10.existsSync(filePath)) {
|
|
4263
|
+
fs10.writeFileSync(filePath, content, "utf8");
|
|
3925
4264
|
}
|
|
3926
4265
|
}
|
|
3927
4266
|
function ensureGitignoreEntry(root, entry) {
|
|
3928
|
-
const gitignorePath =
|
|
3929
|
-
if (!
|
|
3930
|
-
|
|
4267
|
+
const gitignorePath = path13.join(root, ".gitignore");
|
|
4268
|
+
if (!fs10.existsSync(gitignorePath)) {
|
|
4269
|
+
fs10.writeFileSync(gitignorePath, `${entry}
|
|
3931
4270
|
`, "utf8");
|
|
3932
4271
|
return;
|
|
3933
4272
|
}
|
|
3934
|
-
const content =
|
|
4273
|
+
const content = fs10.readFileSync(gitignorePath, "utf8");
|
|
3935
4274
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3936
4275
|
if (!lines.includes(entry)) {
|
|
3937
4276
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3938
|
-
|
|
4277
|
+
fs10.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
3939
4278
|
`, "utf8");
|
|
3940
4279
|
}
|
|
3941
4280
|
}
|
|
3942
4281
|
function ensureGitattributesEntry(root, entry) {
|
|
3943
|
-
const attrsPath =
|
|
3944
|
-
if (!
|
|
3945
|
-
|
|
4282
|
+
const attrsPath = path13.join(root, ".gitattributes");
|
|
4283
|
+
if (!fs10.existsSync(attrsPath)) {
|
|
4284
|
+
fs10.writeFileSync(attrsPath, `${entry}
|
|
3946
4285
|
`, "utf8");
|
|
3947
4286
|
return;
|
|
3948
4287
|
}
|
|
3949
|
-
const content =
|
|
4288
|
+
const content = fs10.readFileSync(attrsPath, "utf8");
|
|
3950
4289
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3951
4290
|
if (!lines.includes(entry)) {
|
|
3952
4291
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3953
|
-
|
|
4292
|
+
fs10.writeFileSync(attrsPath, `${content}${suffix}${entry}
|
|
3954
4293
|
`, "utf8");
|
|
3955
4294
|
}
|
|
3956
4295
|
}
|
|
@@ -4036,7 +4375,7 @@ function registerInitCommand(program) {
|
|
|
4036
4375
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4037
4376
|
const identity = await resolveInitIdentity(root, options);
|
|
4038
4377
|
const projectId = identity.projectId;
|
|
4039
|
-
const projectRoot =
|
|
4378
|
+
const projectRoot = path13.join(workspaceDir, "projects", projectId);
|
|
4040
4379
|
const dirs = [
|
|
4041
4380
|
"ideas",
|
|
4042
4381
|
"tasks",
|
|
@@ -4052,23 +4391,23 @@ function registerInitCommand(program) {
|
|
|
4052
4391
|
"history/deliveries",
|
|
4053
4392
|
".index"
|
|
4054
4393
|
];
|
|
4055
|
-
ensureDir(
|
|
4394
|
+
ensureDir(path13.join(workspaceDir, "projects"));
|
|
4056
4395
|
for (const dir of dirs) {
|
|
4057
|
-
ensureDir(
|
|
4396
|
+
ensureDir(path13.join(projectRoot, dir));
|
|
4058
4397
|
}
|
|
4059
4398
|
writeIfMissing(
|
|
4060
|
-
|
|
4399
|
+
path13.join(projectRoot, "config.yml"),
|
|
4061
4400
|
buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
|
|
4062
4401
|
);
|
|
4063
|
-
if (!
|
|
4402
|
+
if (!fs10.existsSync(path13.join(projectRoot, "schema-version"))) {
|
|
4064
4403
|
write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
|
|
4065
4404
|
}
|
|
4066
|
-
writeIfMissing(
|
|
4067
|
-
writeIfMissing(
|
|
4068
|
-
writeIfMissing(
|
|
4069
|
-
writeIfMissing(
|
|
4070
|
-
writeIfMissing(
|
|
4071
|
-
writeIfMissing(
|
|
4405
|
+
writeIfMissing(path13.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
|
|
4406
|
+
writeIfMissing(path13.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
|
|
4407
|
+
writeIfMissing(path13.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
|
|
4408
|
+
writeIfMissing(path13.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
|
|
4409
|
+
writeIfMissing(path13.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
|
|
4410
|
+
writeIfMissing(path13.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
|
|
4072
4411
|
writeWorkspaceConfig(root, { version: 2, current_project: projectId });
|
|
4073
4412
|
ensureGitignoreEntry(root, ".coop/logs/");
|
|
4074
4413
|
ensureGitignoreEntry(root, ".coop/tmp/");
|
|
@@ -4078,18 +4417,18 @@ function registerInitCommand(program) {
|
|
|
4078
4417
|
const project = resolveProject(root, projectId);
|
|
4079
4418
|
console.log("Initialized COOP workspace.");
|
|
4080
4419
|
console.log(`- Root: ${root}`);
|
|
4081
|
-
console.log(`- Workspace: ${
|
|
4082
|
-
console.log(`- Project: ${project.id} (${
|
|
4420
|
+
console.log(`- Workspace: ${path13.relative(root, workspaceDir)}`);
|
|
4421
|
+
console.log(`- Project: ${project.id} (${path13.relative(root, project.root)})`);
|
|
4083
4422
|
console.log(`- Name: ${identity.projectName}`);
|
|
4084
4423
|
console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
4085
4424
|
console.log(`- ID naming: ${identity.namingTemplate}`);
|
|
4086
4425
|
console.log(`- ${preCommitHook.message}`);
|
|
4087
4426
|
if (preCommitHook.installed) {
|
|
4088
|
-
console.log(`- Hook: ${
|
|
4427
|
+
console.log(`- Hook: ${path13.relative(root, preCommitHook.hookPath)}`);
|
|
4089
4428
|
}
|
|
4090
4429
|
console.log(`- ${postMergeHook.message}`);
|
|
4091
4430
|
if (postMergeHook.installed) {
|
|
4092
|
-
console.log(`- Hook: ${
|
|
4431
|
+
console.log(`- Hook: ${path13.relative(root, postMergeHook.hookPath)}`);
|
|
4093
4432
|
}
|
|
4094
4433
|
console.log(`- ${mergeDrivers}`);
|
|
4095
4434
|
console.log("- Next steps:");
|
|
@@ -4100,8 +4439,8 @@ function registerInitCommand(program) {
|
|
|
4100
4439
|
}
|
|
4101
4440
|
|
|
4102
4441
|
// src/commands/lifecycle.ts
|
|
4103
|
-
import
|
|
4104
|
-
import { parseTaskFile as
|
|
4442
|
+
import path14 from "path";
|
|
4443
|
+
import { parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
4105
4444
|
var lifecycleVerbs = [
|
|
4106
4445
|
{
|
|
4107
4446
|
name: "review",
|
|
@@ -4136,8 +4475,8 @@ var lifecycleVerbs = [
|
|
|
4136
4475
|
];
|
|
4137
4476
|
function currentTaskSelection(root, id) {
|
|
4138
4477
|
const reference = resolveReference(root, id, "task");
|
|
4139
|
-
const filePath =
|
|
4140
|
-
const parsed =
|
|
4478
|
+
const filePath = path14.join(root, ...reference.file.split("/"));
|
|
4479
|
+
const parsed = parseTaskFile9(filePath);
|
|
4141
4480
|
return formatSelectedTask(
|
|
4142
4481
|
{
|
|
4143
4482
|
task: parsed.task,
|
|
@@ -4161,16 +4500,9 @@ function registerLifecycleCommands(program) {
|
|
|
4161
4500
|
}
|
|
4162
4501
|
|
|
4163
4502
|
// src/commands/list.ts
|
|
4164
|
-
import
|
|
4165
|
-
import { parseIdeaFile as
|
|
4503
|
+
import path15 from "path";
|
|
4504
|
+
import { load_graph as load_graph6, parseDeliveryFile as parseDeliveryFile2, parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile10, schedule_next as schedule_next3 } from "@kitsy/coop-core";
|
|
4166
4505
|
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
4506
|
function statusColor(status) {
|
|
4175
4507
|
switch (status) {
|
|
4176
4508
|
case "done":
|
|
@@ -4190,49 +4522,81 @@ function statusColor(status) {
|
|
|
4190
4522
|
function sortByIdAsc(items) {
|
|
4191
4523
|
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
4192
4524
|
}
|
|
4193
|
-
function
|
|
4525
|
+
function loadTasks2(root) {
|
|
4194
4526
|
return listTaskFiles(root).map((filePath) => ({
|
|
4195
|
-
task:
|
|
4527
|
+
task: parseTaskFile10(filePath).task,
|
|
4196
4528
|
filePath
|
|
4197
4529
|
}));
|
|
4198
4530
|
}
|
|
4199
4531
|
function loadIdeas(root) {
|
|
4200
4532
|
return listIdeaFiles(root).map((filePath) => ({
|
|
4201
|
-
idea:
|
|
4533
|
+
idea: parseIdeaFile4(filePath).idea,
|
|
4202
4534
|
filePath
|
|
4203
4535
|
}));
|
|
4204
4536
|
}
|
|
4205
4537
|
function listTasks(options) {
|
|
4206
4538
|
const root = resolveRepoRoot();
|
|
4207
4539
|
ensureCoopInitialized(root);
|
|
4208
|
-
const
|
|
4540
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
4541
|
+
const graph = load_graph6(coopDir(root));
|
|
4542
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track"));
|
|
4543
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
4544
|
+
const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
|
|
4545
|
+
const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
|
|
4546
|
+
if (isVerboseRequested()) {
|
|
4547
|
+
for (const line of formatResolvedContextMessage({
|
|
4548
|
+
track: resolvedTrack,
|
|
4549
|
+
delivery: resolvedDelivery
|
|
4550
|
+
})) {
|
|
4551
|
+
console.log(line);
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
const readyEntries = options.ready ? schedule_next3(load_graph6(ensureCoopInitialized(root)), {
|
|
4555
|
+
track: resolvedTrack.value,
|
|
4556
|
+
delivery: resolvedDelivery.value
|
|
4557
|
+
}) : null;
|
|
4558
|
+
const readyIds = readyEntries ? new Set(readyEntries.map((entry) => entry.task.id)) : null;
|
|
4559
|
+
const readyOrder = readyEntries ? new Map(readyEntries.map((entry, index) => [entry.task.id, index])) : null;
|
|
4560
|
+
const rows = loadTasks2(root).filter(({ task }) => {
|
|
4561
|
+
if (readyIds && !readyIds.has(task.id)) return false;
|
|
4209
4562
|
if (options.status && task.status !== options.status) return false;
|
|
4210
|
-
if (
|
|
4211
|
-
|
|
4563
|
+
if (resolvedTrack.value && task.track !== resolvedTrack.value && !(task.delivery_tracks ?? []).includes(resolvedTrack.value)) {
|
|
4564
|
+
return false;
|
|
4565
|
+
}
|
|
4566
|
+
if (resolvedDelivery.value && task.delivery !== resolvedDelivery.value && !deliveryScope?.has(task.id)) return false;
|
|
4567
|
+
if (options.priority && taskEffectivePriority(task, resolvedTrack.value) !== options.priority) return false;
|
|
4568
|
+
if (assignee && (task.assignee ?? "") !== assignee) return false;
|
|
4569
|
+
if (options.version && !(task.fix_versions ?? []).includes(options.version) && !(task.released_in ?? []).includes(options.version)) {
|
|
4570
|
+
return false;
|
|
4571
|
+
}
|
|
4212
4572
|
return true;
|
|
4213
4573
|
}).map(({ task, filePath }) => ({
|
|
4214
4574
|
id: task.id,
|
|
4215
4575
|
title: task.title,
|
|
4216
4576
|
status: task.status,
|
|
4217
|
-
priority: task.
|
|
4577
|
+
priority: taskEffectivePriority(task, resolvedTrack.value),
|
|
4218
4578
|
track: task.track ?? "-",
|
|
4579
|
+
assignee: task.assignee ?? "-",
|
|
4580
|
+
delivery: task.delivery ?? "-",
|
|
4219
4581
|
filePath
|
|
4220
4582
|
}));
|
|
4221
|
-
const sorted = sortByIdAsc(rows);
|
|
4583
|
+
const sorted = readyOrder ? [...rows].sort((a, b) => (readyOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (readyOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER)) : sortByIdAsc(rows);
|
|
4222
4584
|
if (sorted.length === 0) {
|
|
4223
4585
|
console.log("No tasks found.");
|
|
4224
4586
|
return;
|
|
4225
4587
|
}
|
|
4226
4588
|
console.log(
|
|
4227
4589
|
formatTable(
|
|
4228
|
-
["ID", "Title", "Status", "Priority", "Track", "File"],
|
|
4590
|
+
["ID", "Title", "Status", "Priority", "Track", "Assignee", "Delivery", "File"],
|
|
4229
4591
|
sorted.map((entry) => [
|
|
4230
4592
|
entry.id,
|
|
4231
4593
|
entry.title,
|
|
4232
4594
|
statusColor(entry.status),
|
|
4233
4595
|
entry.priority,
|
|
4234
4596
|
entry.track,
|
|
4235
|
-
|
|
4597
|
+
entry.assignee,
|
|
4598
|
+
entry.delivery,
|
|
4599
|
+
path15.relative(root, entry.filePath)
|
|
4236
4600
|
])
|
|
4237
4601
|
)
|
|
4238
4602
|
);
|
|
@@ -4267,16 +4631,31 @@ function listIdeas(options) {
|
|
|
4267
4631
|
statusColor(entry.status),
|
|
4268
4632
|
entry.priority,
|
|
4269
4633
|
entry.track,
|
|
4270
|
-
|
|
4634
|
+
path15.relative(root, entry.filePath)
|
|
4271
4635
|
])
|
|
4272
4636
|
)
|
|
4273
4637
|
);
|
|
4274
4638
|
console.log(`
|
|
4275
4639
|
Total ideas: ${sorted.length}`);
|
|
4276
4640
|
}
|
|
4641
|
+
function listDeliveries() {
|
|
4642
|
+
const root = resolveRepoRoot();
|
|
4643
|
+
const rows = listDeliveryFiles(root).map((filePath) => ({ delivery: parseDeliveryFile2(filePath).delivery, filePath })).map(({ delivery, filePath }) => [
|
|
4644
|
+
delivery.id,
|
|
4645
|
+
delivery.name,
|
|
4646
|
+
statusColor(delivery.status),
|
|
4647
|
+
delivery.target_date ?? "-",
|
|
4648
|
+
path15.relative(root, filePath)
|
|
4649
|
+
]);
|
|
4650
|
+
if (rows.length === 0) {
|
|
4651
|
+
console.log("No deliveries found.");
|
|
4652
|
+
return;
|
|
4653
|
+
}
|
|
4654
|
+
console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
|
|
4655
|
+
}
|
|
4277
4656
|
function registerListCommand(program) {
|
|
4278
4657
|
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) => {
|
|
4658
|
+
list.command("tasks").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by track, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery, 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").action((options) => {
|
|
4280
4659
|
listTasks(options);
|
|
4281
4660
|
});
|
|
4282
4661
|
list.command("ideas").description("List ideas").option("--status <status>", "Filter by status").action((options) => {
|
|
@@ -4285,32 +4664,32 @@ function registerListCommand(program) {
|
|
|
4285
4664
|
list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
|
|
4286
4665
|
listAliasRows(pattern);
|
|
4287
4666
|
});
|
|
4288
|
-
list.command("deliveries").description("List deliveries
|
|
4289
|
-
|
|
4667
|
+
list.command("deliveries").description("List deliveries").action(() => {
|
|
4668
|
+
listDeliveries();
|
|
4290
4669
|
});
|
|
4291
4670
|
}
|
|
4292
4671
|
|
|
4293
4672
|
// src/utils/logger.ts
|
|
4294
|
-
import
|
|
4295
|
-
import
|
|
4673
|
+
import fs11 from "fs";
|
|
4674
|
+
import path16 from "path";
|
|
4296
4675
|
function resolveRepoSafe(start = process.cwd()) {
|
|
4297
4676
|
try {
|
|
4298
4677
|
return resolveRepoRoot(start);
|
|
4299
4678
|
} catch {
|
|
4300
|
-
return
|
|
4679
|
+
return path16.resolve(start);
|
|
4301
4680
|
}
|
|
4302
4681
|
}
|
|
4303
4682
|
function resolveCliLogFile(start = process.cwd()) {
|
|
4304
4683
|
const root = resolveRepoSafe(start);
|
|
4305
4684
|
const workspace = coopWorkspaceDir(root);
|
|
4306
|
-
if (
|
|
4307
|
-
return
|
|
4685
|
+
if (fs11.existsSync(workspace)) {
|
|
4686
|
+
return path16.join(workspace, "logs", "cli.log");
|
|
4308
4687
|
}
|
|
4309
|
-
return
|
|
4688
|
+
return path16.join(resolveCoopHome(), "logs", "cli.log");
|
|
4310
4689
|
}
|
|
4311
4690
|
function appendLogEntry(entry, logFile) {
|
|
4312
|
-
|
|
4313
|
-
|
|
4691
|
+
fs11.mkdirSync(path16.dirname(logFile), { recursive: true });
|
|
4692
|
+
fs11.appendFileSync(logFile, `${JSON.stringify(entry)}
|
|
4314
4693
|
`, "utf8");
|
|
4315
4694
|
}
|
|
4316
4695
|
function logCliError(error, start = process.cwd()) {
|
|
@@ -4349,8 +4728,8 @@ function parseLogLine(line) {
|
|
|
4349
4728
|
}
|
|
4350
4729
|
function readLastCliLog(start = process.cwd()) {
|
|
4351
4730
|
const logFile = resolveCliLogFile(start);
|
|
4352
|
-
if (!
|
|
4353
|
-
const content =
|
|
4731
|
+
if (!fs11.existsSync(logFile)) return null;
|
|
4732
|
+
const content = fs11.readFileSync(logFile, "utf8");
|
|
4354
4733
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4355
4734
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
4356
4735
|
const entry = parseLogLine(lines[i] ?? "");
|
|
@@ -4384,9 +4763,37 @@ function registerLogCommand(program) {
|
|
|
4384
4763
|
});
|
|
4385
4764
|
}
|
|
4386
4765
|
|
|
4766
|
+
// src/commands/log-time.ts
|
|
4767
|
+
function registerLogTimeCommand(program) {
|
|
4768
|
+
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) => {
|
|
4769
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
4770
|
+
if (entity !== "task") {
|
|
4771
|
+
throw new Error("Only task time logging is supported.");
|
|
4772
|
+
}
|
|
4773
|
+
const hours = Number(options.hours);
|
|
4774
|
+
if (!Number.isFinite(hours) || hours < 0) {
|
|
4775
|
+
throw new Error(`Invalid --hours value '${options.hours}'. Expected a non-negative number.`);
|
|
4776
|
+
}
|
|
4777
|
+
if (options.kind !== "planned" && options.kind !== "worked") {
|
|
4778
|
+
throw new Error(`Invalid --kind value '${options.kind}'. Expected planned|worked.`);
|
|
4779
|
+
}
|
|
4780
|
+
const root = resolveRepoRoot();
|
|
4781
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
4782
|
+
const task = appendTaskTimeLog(
|
|
4783
|
+
parsed.task,
|
|
4784
|
+
options.actor?.trim() || defaultCoopAuthor(root),
|
|
4785
|
+
options.kind,
|
|
4786
|
+
hours,
|
|
4787
|
+
options.note?.trim() || void 0
|
|
4788
|
+
);
|
|
4789
|
+
writeTaskEntry(filePath, parsed, task);
|
|
4790
|
+
console.log(`Logged ${hours}h ${options.kind} on ${task.id}`);
|
|
4791
|
+
});
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4387
4794
|
// src/commands/migrate.ts
|
|
4388
|
-
import
|
|
4389
|
-
import
|
|
4795
|
+
import fs12 from "fs";
|
|
4796
|
+
import path17 from "path";
|
|
4390
4797
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4391
4798
|
import { stdin as input2, stdout as output2 } from "process";
|
|
4392
4799
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
|
|
@@ -4405,22 +4812,22 @@ function parseTargetVersion(raw) {
|
|
|
4405
4812
|
return parsed;
|
|
4406
4813
|
}
|
|
4407
4814
|
function writeIfMissing2(filePath, content) {
|
|
4408
|
-
if (!
|
|
4409
|
-
|
|
4815
|
+
if (!fs12.existsSync(filePath)) {
|
|
4816
|
+
fs12.writeFileSync(filePath, content, "utf8");
|
|
4410
4817
|
}
|
|
4411
4818
|
}
|
|
4412
4819
|
function ensureGitignoreEntry2(root, entry) {
|
|
4413
|
-
const gitignorePath =
|
|
4414
|
-
if (!
|
|
4415
|
-
|
|
4820
|
+
const gitignorePath = path17.join(root, ".gitignore");
|
|
4821
|
+
if (!fs12.existsSync(gitignorePath)) {
|
|
4822
|
+
fs12.writeFileSync(gitignorePath, `${entry}
|
|
4416
4823
|
`, "utf8");
|
|
4417
4824
|
return;
|
|
4418
4825
|
}
|
|
4419
|
-
const content =
|
|
4826
|
+
const content = fs12.readFileSync(gitignorePath, "utf8");
|
|
4420
4827
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
4421
4828
|
if (!lines.includes(entry)) {
|
|
4422
4829
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
4423
|
-
|
|
4830
|
+
fs12.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
4424
4831
|
`, "utf8");
|
|
4425
4832
|
}
|
|
4426
4833
|
}
|
|
@@ -4445,7 +4852,7 @@ function legacyWorkspaceProjectEntries(root) {
|
|
|
4445
4852
|
"backlog",
|
|
4446
4853
|
"plans",
|
|
4447
4854
|
"releases"
|
|
4448
|
-
].filter((entry) =>
|
|
4855
|
+
].filter((entry) => fs12.existsSync(path17.join(workspaceDir, entry)));
|
|
4449
4856
|
}
|
|
4450
4857
|
function normalizeProjectId2(value) {
|
|
4451
4858
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
@@ -4490,7 +4897,7 @@ async function promptProjectIdentity(defaults, options) {
|
|
|
4490
4897
|
async function resolveMigrationIdentity(root, options) {
|
|
4491
4898
|
const existing = readCoopConfig(root);
|
|
4492
4899
|
const defaults = {
|
|
4493
|
-
projectName: existing.projectName ||
|
|
4900
|
+
projectName: existing.projectName || path17.basename(root),
|
|
4494
4901
|
projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
|
|
4495
4902
|
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
|
|
4496
4903
|
};
|
|
@@ -4510,22 +4917,22 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4510
4917
|
throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
|
|
4511
4918
|
}
|
|
4512
4919
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4513
|
-
if (!
|
|
4920
|
+
if (!fs12.existsSync(workspaceDir)) {
|
|
4514
4921
|
throw new Error("Missing .coop directory. Run 'coop init' first.");
|
|
4515
4922
|
}
|
|
4516
|
-
const projectsDir =
|
|
4923
|
+
const projectsDir = path17.join(workspaceDir, "projects");
|
|
4517
4924
|
const legacyEntries = legacyWorkspaceProjectEntries(root);
|
|
4518
|
-
if (legacyEntries.length === 0 &&
|
|
4925
|
+
if (legacyEntries.length === 0 && fs12.existsSync(projectsDir)) {
|
|
4519
4926
|
console.log("[COOP] workspace layout already uses v2.");
|
|
4520
4927
|
return;
|
|
4521
4928
|
}
|
|
4522
4929
|
const identity = await resolveMigrationIdentity(root, options);
|
|
4523
4930
|
const projectId = identity.projectId;
|
|
4524
|
-
const projectRoot =
|
|
4525
|
-
if (
|
|
4526
|
-
throw new Error(`Project destination '${
|
|
4931
|
+
const projectRoot = path17.join(projectsDir, projectId);
|
|
4932
|
+
if (fs12.existsSync(projectRoot) && !options.force) {
|
|
4933
|
+
throw new Error(`Project destination '${path17.relative(root, projectRoot)}' already exists. Re-run with --force.`);
|
|
4527
4934
|
}
|
|
4528
|
-
const changed = legacyEntries.map((entry) => `${
|
|
4935
|
+
const changed = legacyEntries.map((entry) => `${path17.join(".coop", entry)} -> ${path17.join(".coop", "projects", projectId, entry)}`);
|
|
4529
4936
|
changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
|
|
4530
4937
|
console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
|
|
4531
4938
|
console.log(`- from: v1 flat layout`);
|
|
@@ -4541,21 +4948,21 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4541
4948
|
console.log("- no files were modified.");
|
|
4542
4949
|
return;
|
|
4543
4950
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4951
|
+
fs12.mkdirSync(projectsDir, { recursive: true });
|
|
4952
|
+
fs12.mkdirSync(projectRoot, { recursive: true });
|
|
4546
4953
|
for (const entry of legacyEntries) {
|
|
4547
|
-
const source =
|
|
4548
|
-
const destination =
|
|
4549
|
-
if (
|
|
4954
|
+
const source = path17.join(workspaceDir, entry);
|
|
4955
|
+
const destination = path17.join(projectRoot, entry);
|
|
4956
|
+
if (fs12.existsSync(destination)) {
|
|
4550
4957
|
if (!options.force) {
|
|
4551
|
-
throw new Error(`Destination '${
|
|
4958
|
+
throw new Error(`Destination '${path17.relative(root, destination)}' already exists.`);
|
|
4552
4959
|
}
|
|
4553
|
-
|
|
4960
|
+
fs12.rmSync(destination, { recursive: true, force: true });
|
|
4554
4961
|
}
|
|
4555
|
-
|
|
4962
|
+
fs12.renameSync(source, destination);
|
|
4556
4963
|
}
|
|
4557
|
-
const movedConfigPath =
|
|
4558
|
-
if (
|
|
4964
|
+
const movedConfigPath = path17.join(projectRoot, "config.yml");
|
|
4965
|
+
if (fs12.existsSync(movedConfigPath)) {
|
|
4559
4966
|
const movedConfig = parseYamlFile2(movedConfigPath);
|
|
4560
4967
|
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
4561
4968
|
nextProject.name = identity.projectName;
|
|
@@ -4572,13 +4979,13 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4572
4979
|
}
|
|
4573
4980
|
const workspace = readWorkspaceConfig(root);
|
|
4574
4981
|
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
|
|
4575
|
-
writeIfMissing2(
|
|
4576
|
-
writeIfMissing2(
|
|
4982
|
+
writeIfMissing2(path17.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
|
|
4983
|
+
writeIfMissing2(path17.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
|
|
4577
4984
|
ensureGitignoreEntry2(root, ".coop/logs/");
|
|
4578
4985
|
ensureGitignoreEntry2(root, ".coop/tmp/");
|
|
4579
4986
|
const manager = new IndexManager3(projectRoot);
|
|
4580
4987
|
manager.build_full_index();
|
|
4581
|
-
console.log(`[COOP] migrated workspace to v2 at ${
|
|
4988
|
+
console.log(`[COOP] migrated workspace to v2 at ${path17.relative(root, projectRoot)}`);
|
|
4582
4989
|
}
|
|
4583
4990
|
function registerMigrateCommand(program) {
|
|
4584
4991
|
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 +5004,7 @@ function registerMigrateCommand(program) {
|
|
|
4597
5004
|
if (report.changed_files.length > 0) {
|
|
4598
5005
|
console.log("- changed files:");
|
|
4599
5006
|
for (const filePath of report.changed_files) {
|
|
4600
|
-
console.log(` - ${
|
|
5007
|
+
console.log(` - ${path17.relative(root, filePath)}`);
|
|
4601
5008
|
}
|
|
4602
5009
|
}
|
|
4603
5010
|
if (report.dry_run) {
|
|
@@ -4691,7 +5098,7 @@ import chalk3 from "chalk";
|
|
|
4691
5098
|
import {
|
|
4692
5099
|
analyze_feasibility,
|
|
4693
5100
|
analyze_what_if,
|
|
4694
|
-
load_graph as
|
|
5101
|
+
load_graph as load_graph7,
|
|
4695
5102
|
monte_carlo_forecast,
|
|
4696
5103
|
TaskPriority as TaskPriority2
|
|
4697
5104
|
} from "@kitsy/coop-core";
|
|
@@ -4755,8 +5162,19 @@ function collectWhatIfModifications(options) {
|
|
|
4755
5162
|
}
|
|
4756
5163
|
async function runPlanDelivery(deliveryName, options) {
|
|
4757
5164
|
const root = resolveRepoRoot();
|
|
4758
|
-
const
|
|
4759
|
-
const
|
|
5165
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5166
|
+
const graph = load_graph7(coopDir(root));
|
|
5167
|
+
const resolvedDelivery = deliveryName?.trim() ? { value: deliveryName.trim(), source: "arg" } : resolveContextValueWithSource(void 0, context.delivery, sharedDefault(root, "delivery"));
|
|
5168
|
+
if (isVerboseRequested()) {
|
|
5169
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
5170
|
+
console.log(line);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
const deliveryRef = resolvedDelivery.value;
|
|
5174
|
+
if (!deliveryRef) {
|
|
5175
|
+
throw new Error("delivery is not set; pass a delivery id or set a default working delivery with `coop use delivery <id>`.");
|
|
5176
|
+
}
|
|
5177
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
4760
5178
|
const modifications = collectWhatIfModifications(options);
|
|
4761
5179
|
if (modifications.length > 0) {
|
|
4762
5180
|
const comparison = analyze_what_if(
|
|
@@ -4871,7 +5289,7 @@ async function runPlanDelivery(deliveryName, options) {
|
|
|
4871
5289
|
}
|
|
4872
5290
|
function runPlanCapacity(trackArg, options) {
|
|
4873
5291
|
const root = resolveRepoRoot();
|
|
4874
|
-
const graph =
|
|
5292
|
+
const graph = load_graph7(coopDir(root));
|
|
4875
5293
|
const track = normalizeTrack(trackArg);
|
|
4876
5294
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
4877
5295
|
if (deliveries.length === 0) {
|
|
@@ -4911,17 +5329,43 @@ function runPlanCapacity(trackArg, options) {
|
|
|
4911
5329
|
}
|
|
4912
5330
|
function registerPlanCommand(program) {
|
|
4913
5331
|
const plan = program.command("plan").description("Planning commands");
|
|
4914
|
-
plan.command("delivery").description("Analyze delivery feasibility").argument("
|
|
4915
|
-
await runPlanDelivery(name, options);
|
|
5332
|
+
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) => {
|
|
5333
|
+
await runPlanDelivery(name ?? "", options);
|
|
4916
5334
|
});
|
|
4917
5335
|
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
5336
|
runPlanCapacity(track, options);
|
|
4919
5337
|
});
|
|
4920
5338
|
}
|
|
4921
5339
|
|
|
5340
|
+
// src/commands/promote.ts
|
|
5341
|
+
function registerPromoteCommand(program) {
|
|
5342
|
+
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) => {
|
|
5343
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
5344
|
+
if (entity !== "task") {
|
|
5345
|
+
throw new Error("Only task promotion is supported.");
|
|
5346
|
+
}
|
|
5347
|
+
const root = resolveRepoRoot();
|
|
5348
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5349
|
+
const resolvedTrack = resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track"));
|
|
5350
|
+
const resolvedVersion = resolveContextValueWithSource(options.version, context.version, sharedDefault(root, "version"));
|
|
5351
|
+
if (isVerboseRequested()) {
|
|
5352
|
+
for (const line of formatResolvedContextMessage({ track: resolvedTrack, version: resolvedVersion })) {
|
|
5353
|
+
console.log(line);
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
5357
|
+
const task = promoteTaskForContext(parsed.task, {
|
|
5358
|
+
track: resolvedTrack.value,
|
|
5359
|
+
version: resolvedVersion.value
|
|
5360
|
+
});
|
|
5361
|
+
writeTaskEntry(filePath, parsed, task);
|
|
5362
|
+
console.log(`Promoted ${task.id}`);
|
|
5363
|
+
});
|
|
5364
|
+
}
|
|
5365
|
+
|
|
4922
5366
|
// src/commands/project.ts
|
|
4923
|
-
import
|
|
4924
|
-
import
|
|
5367
|
+
import fs13 from "fs";
|
|
5368
|
+
import path18 from "path";
|
|
4925
5369
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
|
|
4926
5370
|
var TASK_TEMPLATE2 = `---
|
|
4927
5371
|
id: TASK-001
|
|
@@ -5030,11 +5474,11 @@ var PROJECT_DIRS = [
|
|
|
5030
5474
|
".index"
|
|
5031
5475
|
];
|
|
5032
5476
|
function ensureDir2(dirPath) {
|
|
5033
|
-
|
|
5477
|
+
fs13.mkdirSync(dirPath, { recursive: true });
|
|
5034
5478
|
}
|
|
5035
5479
|
function writeIfMissing3(filePath, content) {
|
|
5036
|
-
if (!
|
|
5037
|
-
|
|
5480
|
+
if (!fs13.existsSync(filePath)) {
|
|
5481
|
+
fs13.writeFileSync(filePath, content, "utf8");
|
|
5038
5482
|
}
|
|
5039
5483
|
}
|
|
5040
5484
|
function normalizeProjectId3(value) {
|
|
@@ -5042,17 +5486,17 @@ function normalizeProjectId3(value) {
|
|
|
5042
5486
|
}
|
|
5043
5487
|
function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID_NAMING_TEMPLATE) {
|
|
5044
5488
|
const workspaceDir = coopWorkspaceDir(root);
|
|
5045
|
-
const projectRoot =
|
|
5046
|
-
ensureDir2(
|
|
5489
|
+
const projectRoot = path18.join(workspaceDir, "projects", projectId);
|
|
5490
|
+
ensureDir2(path18.join(workspaceDir, "projects"));
|
|
5047
5491
|
for (const dir of PROJECT_DIRS) {
|
|
5048
|
-
ensureDir2(
|
|
5492
|
+
ensureDir2(path18.join(projectRoot, dir));
|
|
5049
5493
|
}
|
|
5050
|
-
writeIfMissing3(
|
|
5051
|
-
if (!
|
|
5494
|
+
writeIfMissing3(path18.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
|
|
5495
|
+
if (!fs13.existsSync(path18.join(projectRoot, "schema-version"))) {
|
|
5052
5496
|
write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
|
|
5053
5497
|
}
|
|
5054
|
-
writeIfMissing3(
|
|
5055
|
-
writeIfMissing3(
|
|
5498
|
+
writeIfMissing3(path18.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
|
|
5499
|
+
writeIfMissing3(path18.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
|
|
5056
5500
|
return projectRoot;
|
|
5057
5501
|
}
|
|
5058
5502
|
function registerProjectCommand(program) {
|
|
@@ -5080,7 +5524,7 @@ function registerProjectCommand(program) {
|
|
|
5080
5524
|
}
|
|
5081
5525
|
console.log(`id=${active.id}`);
|
|
5082
5526
|
console.log(`name=${active.name}`);
|
|
5083
|
-
console.log(`path=${
|
|
5527
|
+
console.log(`path=${path18.relative(root, active.root)}`);
|
|
5084
5528
|
console.log(`layout=${active.layout}`);
|
|
5085
5529
|
});
|
|
5086
5530
|
project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
|
|
@@ -5112,26 +5556,127 @@ function registerProjectCommand(program) {
|
|
|
5112
5556
|
version: 2,
|
|
5113
5557
|
current_project: workspace.current_project || projectId
|
|
5114
5558
|
});
|
|
5115
|
-
console.log(`Created project '${projectId}' at ${
|
|
5559
|
+
console.log(`Created project '${projectId}' at ${path18.relative(root, projectRoot)}`);
|
|
5560
|
+
});
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/commands/prompt.ts
|
|
5564
|
+
import fs14 from "fs";
|
|
5565
|
+
function buildPayload(root, id) {
|
|
5566
|
+
const { parsed } = loadTaskEntry(root, id);
|
|
5567
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5568
|
+
const task = parsed.task;
|
|
5569
|
+
return {
|
|
5570
|
+
task: {
|
|
5571
|
+
id: task.id,
|
|
5572
|
+
title: task.title,
|
|
5573
|
+
status: task.status,
|
|
5574
|
+
type: task.type,
|
|
5575
|
+
track: task.track ?? null,
|
|
5576
|
+
delivery_tracks: task.delivery_tracks ?? [],
|
|
5577
|
+
delivery: task.delivery ?? null,
|
|
5578
|
+
priority: task.priority ?? null,
|
|
5579
|
+
effective_priority: taskEffectivePriority(task, context.track),
|
|
5580
|
+
fix_versions: task.fix_versions ?? [],
|
|
5581
|
+
released_in: task.released_in ?? [],
|
|
5582
|
+
acceptance: task.acceptance ?? [],
|
|
5583
|
+
tests_required: task.tests_required ?? [],
|
|
5584
|
+
refs: {
|
|
5585
|
+
authority: task.origin?.authority_refs ?? [],
|
|
5586
|
+
derived: task.origin?.derived_refs ?? []
|
|
5587
|
+
},
|
|
5588
|
+
execution: task.execution ?? null
|
|
5589
|
+
},
|
|
5590
|
+
working_context: context,
|
|
5591
|
+
body: parsed.body.trim()
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
function renderMarkdown(payload) {
|
|
5595
|
+
const lines = [
|
|
5596
|
+
`# Task Prompt: ${payload.task.id}`,
|
|
5597
|
+
"",
|
|
5598
|
+
`- Title: ${payload.task.title}`,
|
|
5599
|
+
`- Status: ${payload.task.status}`,
|
|
5600
|
+
`- Type: ${payload.task.type}`,
|
|
5601
|
+
`- Home track: ${payload.task.track ?? "-"}`,
|
|
5602
|
+
`- Delivery tracks: ${payload.task.delivery_tracks.length > 0 ? payload.task.delivery_tracks.join(", ") : "-"}`,
|
|
5603
|
+
`- Delivery: ${payload.task.delivery ?? "-"}`,
|
|
5604
|
+
`- Effective priority: ${payload.task.effective_priority}`,
|
|
5605
|
+
`- Fix versions: ${payload.task.fix_versions.length > 0 ? payload.task.fix_versions.join(", ") : "-"}`,
|
|
5606
|
+
`- Released in: ${payload.task.released_in.length > 0 ? payload.task.released_in.join(", ") : "-"}`,
|
|
5607
|
+
"",
|
|
5608
|
+
"## Acceptance",
|
|
5609
|
+
...payload.task.acceptance.length > 0 ? payload.task.acceptance.map((entry) => `- ${entry}`) : ["- none"],
|
|
5610
|
+
"",
|
|
5611
|
+
"## Tests Required",
|
|
5612
|
+
...payload.task.tests_required.length > 0 ? payload.task.tests_required.map((entry) => `- ${entry}`) : ["- none"],
|
|
5613
|
+
"",
|
|
5614
|
+
"## Refs",
|
|
5615
|
+
`- Authority: ${payload.task.refs.authority.length > 0 ? payload.task.refs.authority.join(", ") : "-"}`,
|
|
5616
|
+
`- Derived: ${payload.task.refs.derived.length > 0 ? payload.task.refs.derived.join(", ") : "-"}`,
|
|
5617
|
+
"",
|
|
5618
|
+
"## Working Context",
|
|
5619
|
+
`- Track: ${payload.working_context.track ?? "-"}`,
|
|
5620
|
+
`- Delivery: ${payload.working_context.delivery ?? "-"}`,
|
|
5621
|
+
`- Version: ${payload.working_context.version ?? "-"}`,
|
|
5622
|
+
"",
|
|
5623
|
+
"## Task Body",
|
|
5624
|
+
payload.body || "-"
|
|
5625
|
+
];
|
|
5626
|
+
if (payload.task.execution) {
|
|
5627
|
+
lines.push("", "## Execution Hints", "```json", JSON.stringify(payload.task.execution, null, 2), "```");
|
|
5628
|
+
}
|
|
5629
|
+
return `${lines.join("\n")}
|
|
5630
|
+
`;
|
|
5631
|
+
}
|
|
5632
|
+
function registerPromptCommand(program) {
|
|
5633
|
+
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) => {
|
|
5634
|
+
const { entity, id } = resolveOptionalEntityArg(first, second, ["task"], "task");
|
|
5635
|
+
if (entity !== "task") {
|
|
5636
|
+
throw new Error("Only task prompts are supported.");
|
|
5637
|
+
}
|
|
5638
|
+
const root = resolveRepoRoot();
|
|
5639
|
+
const payload = buildPayload(root, id);
|
|
5640
|
+
const format = options.format ?? "text";
|
|
5641
|
+
let output3 = "";
|
|
5642
|
+
if (format === "json") {
|
|
5643
|
+
output3 = `${JSON.stringify(payload, null, 2)}
|
|
5644
|
+
`;
|
|
5645
|
+
} else {
|
|
5646
|
+
output3 = renderMarkdown(payload);
|
|
5647
|
+
}
|
|
5648
|
+
if (options.save) {
|
|
5649
|
+
fs14.writeFileSync(options.save, output3, "utf8");
|
|
5650
|
+
}
|
|
5651
|
+
if (isVerboseRequested()) {
|
|
5652
|
+
for (const line of formatResolvedContextMessage({
|
|
5653
|
+
track: payload.working_context.track ? { value: payload.working_context.track, source: "use" } : void 0,
|
|
5654
|
+
delivery: payload.working_context.delivery ? { value: payload.working_context.delivery, source: "use" } : void 0,
|
|
5655
|
+
version: payload.working_context.version ? { value: payload.working_context.version, source: "use" } : void 0
|
|
5656
|
+
})) {
|
|
5657
|
+
console.log(line);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
console.log(output3.trimEnd());
|
|
5116
5661
|
});
|
|
5117
5662
|
}
|
|
5118
5663
|
|
|
5119
5664
|
// src/commands/refine.ts
|
|
5120
|
-
import
|
|
5121
|
-
import
|
|
5122
|
-
import { parseIdeaFile as
|
|
5665
|
+
import fs15 from "fs";
|
|
5666
|
+
import path19 from "path";
|
|
5667
|
+
import { parseIdeaFile as parseIdeaFile5, parseTaskFile as parseTaskFile11 } from "@kitsy/coop-core";
|
|
5123
5668
|
import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
|
|
5124
|
-
function
|
|
5669
|
+
function resolveTaskFile2(root, idOrAlias) {
|
|
5125
5670
|
const target = resolveReference(root, idOrAlias, "task");
|
|
5126
|
-
return
|
|
5671
|
+
return path19.join(root, ...target.file.split("/"));
|
|
5127
5672
|
}
|
|
5128
|
-
function
|
|
5673
|
+
function resolveIdeaFile3(root, idOrAlias) {
|
|
5129
5674
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
5130
|
-
return
|
|
5675
|
+
return path19.join(root, ...target.file.split("/"));
|
|
5131
5676
|
}
|
|
5132
5677
|
async function readSupplementalInput(root, options) {
|
|
5133
5678
|
if (options.inputFile?.trim()) {
|
|
5134
|
-
return
|
|
5679
|
+
return fs15.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
|
|
5135
5680
|
}
|
|
5136
5681
|
if (options.stdin) {
|
|
5137
5682
|
return readStdinText();
|
|
@@ -5149,11 +5694,11 @@ function loadAuthorityContext(root, refs) {
|
|
|
5149
5694
|
for (const ref of refs ?? []) {
|
|
5150
5695
|
const filePart = extractRefFile(ref);
|
|
5151
5696
|
if (!filePart) continue;
|
|
5152
|
-
const fullPath =
|
|
5153
|
-
if (!
|
|
5697
|
+
const fullPath = path19.resolve(root, filePart);
|
|
5698
|
+
if (!fs15.existsSync(fullPath) || !fs15.statSync(fullPath).isFile()) continue;
|
|
5154
5699
|
out.push({
|
|
5155
5700
|
ref,
|
|
5156
|
-
content:
|
|
5701
|
+
content: fs15.readFileSync(fullPath, "utf8")
|
|
5157
5702
|
});
|
|
5158
5703
|
}
|
|
5159
5704
|
return out;
|
|
@@ -5163,8 +5708,8 @@ function registerRefineCommand(program) {
|
|
|
5163
5708
|
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
5709
|
const root = resolveRepoRoot();
|
|
5165
5710
|
const projectDir = ensureCoopInitialized(root);
|
|
5166
|
-
const ideaFile =
|
|
5167
|
-
const parsed =
|
|
5711
|
+
const ideaFile = resolveIdeaFile3(root, id);
|
|
5712
|
+
const parsed = parseIdeaFile5(ideaFile);
|
|
5168
5713
|
const supplemental = await readSupplementalInput(root, options);
|
|
5169
5714
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
5170
5715
|
const refined = await refine_idea_to_draft(
|
|
@@ -5187,8 +5732,8 @@ function registerRefineCommand(program) {
|
|
|
5187
5732
|
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
5733
|
const root = resolveRepoRoot();
|
|
5189
5734
|
const projectDir = ensureCoopInitialized(root);
|
|
5190
|
-
const taskFile =
|
|
5191
|
-
const parsed =
|
|
5735
|
+
const taskFile = resolveTaskFile2(root, id);
|
|
5736
|
+
const parsed = parseTaskFile11(taskFile);
|
|
5192
5737
|
const supplemental = await readSupplementalInput(root, options);
|
|
5193
5738
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
5194
5739
|
const draft = await refine_task_to_draft(
|
|
@@ -5216,15 +5761,15 @@ function registerRefineCommand(program) {
|
|
|
5216
5761
|
const written = applyRefinementDraft(root, projectDir, draft);
|
|
5217
5762
|
console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
|
|
5218
5763
|
for (const filePath of written) {
|
|
5219
|
-
console.log(`- ${
|
|
5764
|
+
console.log(`- ${path19.relative(root, filePath)}`);
|
|
5220
5765
|
}
|
|
5221
5766
|
});
|
|
5222
5767
|
}
|
|
5223
5768
|
|
|
5224
5769
|
// src/commands/run.ts
|
|
5225
|
-
import
|
|
5226
|
-
import
|
|
5227
|
-
import { load_graph as
|
|
5770
|
+
import fs16 from "fs";
|
|
5771
|
+
import path20 from "path";
|
|
5772
|
+
import { load_graph as load_graph8, parseTaskFile as parseTaskFile12 } from "@kitsy/coop-core";
|
|
5228
5773
|
import {
|
|
5229
5774
|
build_contract,
|
|
5230
5775
|
create_provider_agent_client,
|
|
@@ -5233,11 +5778,11 @@ import {
|
|
|
5233
5778
|
} from "@kitsy/coop-ai";
|
|
5234
5779
|
function loadTask(root, idOrAlias) {
|
|
5235
5780
|
const target = resolveReference(root, idOrAlias, "task");
|
|
5236
|
-
const taskFile =
|
|
5237
|
-
if (!
|
|
5781
|
+
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5782
|
+
if (!fs16.existsSync(taskFile)) {
|
|
5238
5783
|
throw new Error(`Task file not found: ${target.file}`);
|
|
5239
5784
|
}
|
|
5240
|
-
return
|
|
5785
|
+
return parseTaskFile12(taskFile).task;
|
|
5241
5786
|
}
|
|
5242
5787
|
function printContract(contract) {
|
|
5243
5788
|
console.log(JSON.stringify(contract, null, 2));
|
|
@@ -5250,7 +5795,7 @@ function registerRunCommand(program) {
|
|
|
5250
5795
|
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
5796
|
const root = resolveRepoRoot();
|
|
5252
5797
|
const coop = ensureCoopInitialized(root);
|
|
5253
|
-
const graph =
|
|
5798
|
+
const graph = load_graph8(coopDir(root));
|
|
5254
5799
|
const task = loadTask(root, id);
|
|
5255
5800
|
const config = readCoopConfig(root).raw;
|
|
5256
5801
|
const routedAgent = select_agent(task, config);
|
|
@@ -5273,26 +5818,141 @@ function registerRunCommand(program) {
|
|
|
5273
5818
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
5274
5819
|
});
|
|
5275
5820
|
if (result.status === "failed") {
|
|
5276
|
-
throw new Error(`Run failed: ${result.run.id}. Log: ${
|
|
5821
|
+
throw new Error(`Run failed: ${result.run.id}. Log: ${path20.relative(root, result.log_path)}`);
|
|
5277
5822
|
}
|
|
5278
5823
|
if (result.status === "paused") {
|
|
5279
5824
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
5280
|
-
console.log(`[COOP] log: ${
|
|
5825
|
+
console.log(`[COOP] log: ${path20.relative(root, result.log_path)}`);
|
|
5281
5826
|
return;
|
|
5282
5827
|
}
|
|
5283
5828
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
5284
|
-
console.log(`[COOP] log: ${
|
|
5829
|
+
console.log(`[COOP] log: ${path20.relative(root, result.log_path)}`);
|
|
5830
|
+
});
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
// src/commands/search.ts
|
|
5834
|
+
import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile3, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
|
|
5835
|
+
function haystackForTask(task) {
|
|
5836
|
+
return [
|
|
5837
|
+
task.id,
|
|
5838
|
+
task.title,
|
|
5839
|
+
...task.aliases ?? [],
|
|
5840
|
+
...task.tags ?? [],
|
|
5841
|
+
...task.acceptance ?? [],
|
|
5842
|
+
...task.tests_required ?? [],
|
|
5843
|
+
...task.fix_versions ?? [],
|
|
5844
|
+
...task.released_in ?? [],
|
|
5845
|
+
...task.delivery_tracks ?? [],
|
|
5846
|
+
...task.origin?.authority_refs ?? [],
|
|
5847
|
+
...task.origin?.derived_refs ?? [],
|
|
5848
|
+
...task.comments?.map((entry) => entry.body) ?? []
|
|
5849
|
+
].join("\n").toLowerCase();
|
|
5850
|
+
}
|
|
5851
|
+
function haystackForIdea(idea, body) {
|
|
5852
|
+
return [idea.id, idea.title, ...idea.aliases ?? [], ...idea.tags, ...idea.linked_tasks, body].join("\n").toLowerCase();
|
|
5853
|
+
}
|
|
5854
|
+
function haystackForDelivery(delivery, body) {
|
|
5855
|
+
return [
|
|
5856
|
+
delivery.id,
|
|
5857
|
+
delivery.name,
|
|
5858
|
+
delivery.status,
|
|
5859
|
+
...delivery.scope.include ?? [],
|
|
5860
|
+
...delivery.scope.exclude ?? [],
|
|
5861
|
+
...delivery.capacity_profiles ?? [],
|
|
5862
|
+
body
|
|
5863
|
+
].join("\n").toLowerCase();
|
|
5864
|
+
}
|
|
5865
|
+
function includesQuery(haystack, query) {
|
|
5866
|
+
return haystack.includes(query.toLowerCase());
|
|
5867
|
+
}
|
|
5868
|
+
function registerSearchCommand(program) {
|
|
5869
|
+
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) => {
|
|
5870
|
+
const root = resolveRepoRoot();
|
|
5871
|
+
const graph = load_graph9(coopDir(root));
|
|
5872
|
+
const deliveryScope = options.delivery ? new Set(graph.deliveries.get(options.delivery)?.scope.include ?? []) : null;
|
|
5873
|
+
const limit = Number(options.limit ?? "20");
|
|
5874
|
+
const rows = [];
|
|
5875
|
+
if (options.kind === "all" || options.kind === "task" || !options.kind) {
|
|
5876
|
+
for (const filePath of listTaskFiles(root)) {
|
|
5877
|
+
const parsed = parseTaskFile13(filePath);
|
|
5878
|
+
const task = parsed.task;
|
|
5879
|
+
if (options.status && task.status !== options.status) continue;
|
|
5880
|
+
if (options.track && task.track !== options.track && !(task.delivery_tracks ?? []).includes(options.track)) {
|
|
5881
|
+
continue;
|
|
5882
|
+
}
|
|
5883
|
+
if (options.delivery && task.delivery !== options.delivery && !deliveryScope?.has(task.id)) continue;
|
|
5884
|
+
if (options.version && !(task.fix_versions ?? []).includes(options.version) && !(task.released_in ?? []).includes(options.version)) {
|
|
5885
|
+
continue;
|
|
5886
|
+
}
|
|
5887
|
+
if (!includesQuery(`${haystackForTask(task)}
|
|
5888
|
+
${parsed.body}`, query)) continue;
|
|
5889
|
+
rows.push({
|
|
5890
|
+
kind: "task",
|
|
5891
|
+
id: task.id,
|
|
5892
|
+
title: task.title,
|
|
5893
|
+
status: task.status,
|
|
5894
|
+
extra: task.track ?? "-"
|
|
5895
|
+
});
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
if (options.kind === "all" || options.kind === "idea") {
|
|
5899
|
+
for (const filePath of listIdeaFiles(root)) {
|
|
5900
|
+
const parsed = parseIdeaFile6(filePath);
|
|
5901
|
+
if (options.status && parsed.idea.status !== options.status) continue;
|
|
5902
|
+
if (!includesQuery(haystackForIdea(parsed.idea, parsed.body), query)) continue;
|
|
5903
|
+
rows.push({
|
|
5904
|
+
kind: "idea",
|
|
5905
|
+
id: parsed.idea.id,
|
|
5906
|
+
title: parsed.idea.title,
|
|
5907
|
+
status: parsed.idea.status
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
if (options.kind === "all" || options.kind === "delivery") {
|
|
5912
|
+
for (const filePath of listDeliveryFiles(root)) {
|
|
5913
|
+
const parsed = parseDeliveryFile3(filePath);
|
|
5914
|
+
if (options.status && parsed.delivery.status !== options.status) continue;
|
|
5915
|
+
if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
|
|
5916
|
+
rows.push({
|
|
5917
|
+
kind: "delivery",
|
|
5918
|
+
id: parsed.delivery.id,
|
|
5919
|
+
title: parsed.delivery.name,
|
|
5920
|
+
status: parsed.delivery.status,
|
|
5921
|
+
extra: parsed.delivery.target_date ?? "-"
|
|
5922
|
+
});
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
rows.sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`));
|
|
5926
|
+
if (options.open) {
|
|
5927
|
+
if (rows.length === 0) {
|
|
5928
|
+
console.log("No matches found.");
|
|
5929
|
+
return;
|
|
5930
|
+
}
|
|
5931
|
+
if (rows.length !== 1) {
|
|
5932
|
+
throw new Error(`--open requires exactly one match, found ${rows.length}. Narrow the query or add filters.`);
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
for (const row of rows.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 20)) {
|
|
5936
|
+
const suffix = row.extra ? ` | ${row.extra}` : "";
|
|
5937
|
+
console.log(`${row.kind} ${row.id} ${row.status} ${row.title}${suffix}`);
|
|
5938
|
+
if (options.open) {
|
|
5939
|
+
break;
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
if (rows.length === 0) {
|
|
5943
|
+
console.log("No matches found.");
|
|
5944
|
+
}
|
|
5285
5945
|
});
|
|
5286
5946
|
}
|
|
5287
5947
|
|
|
5288
5948
|
// src/server/api.ts
|
|
5289
|
-
import
|
|
5949
|
+
import fs17 from "fs";
|
|
5290
5950
|
import http2 from "http";
|
|
5291
|
-
import
|
|
5951
|
+
import path21 from "path";
|
|
5292
5952
|
import {
|
|
5293
5953
|
analyze_feasibility as analyze_feasibility2,
|
|
5294
|
-
load_graph as
|
|
5295
|
-
parseTaskFile as
|
|
5954
|
+
load_graph as load_graph10,
|
|
5955
|
+
parseTaskFile as parseTaskFile14,
|
|
5296
5956
|
resolve_external_dependencies
|
|
5297
5957
|
} from "@kitsy/coop-core";
|
|
5298
5958
|
function json(res, statusCode, payload) {
|
|
@@ -5334,12 +5994,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
5334
5994
|
};
|
|
5335
5995
|
}
|
|
5336
5996
|
function taskFileById(root, id) {
|
|
5337
|
-
const tasksDir =
|
|
5338
|
-
if (!
|
|
5339
|
-
const entries =
|
|
5997
|
+
const tasksDir = path21.join(resolveProject(root).root, "tasks");
|
|
5998
|
+
if (!fs17.existsSync(tasksDir)) return null;
|
|
5999
|
+
const entries = fs17.readdirSync(tasksDir, { withFileTypes: true });
|
|
5340
6000
|
const target = `${id}.md`.toLowerCase();
|
|
5341
6001
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
5342
|
-
return match ?
|
|
6002
|
+
return match ? path21.join(tasksDir, match.name) : null;
|
|
5343
6003
|
}
|
|
5344
6004
|
function loadRemoteConfig(root) {
|
|
5345
6005
|
const raw = readCoopConfig(root).raw;
|
|
@@ -5405,7 +6065,7 @@ function createApiServer(root, options = {}) {
|
|
|
5405
6065
|
}
|
|
5406
6066
|
const project = resolveProject(repoRoot);
|
|
5407
6067
|
const coopPath = project.root;
|
|
5408
|
-
const graph =
|
|
6068
|
+
const graph = load_graph10(coopPath);
|
|
5409
6069
|
const resolutions = await externalResolutions(repoRoot, graph, options);
|
|
5410
6070
|
if (pathname === "/api/tasks") {
|
|
5411
6071
|
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 +6080,7 @@ function createApiServer(root, options = {}) {
|
|
|
5420
6080
|
notFound(res, `Task '${taskId}' not found.`);
|
|
5421
6081
|
return;
|
|
5422
6082
|
}
|
|
5423
|
-
const parsed =
|
|
6083
|
+
const parsed = parseTaskFile14(filePath);
|
|
5424
6084
|
const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
|
|
5425
6085
|
json(res, 200, {
|
|
5426
6086
|
...workspaceMeta(repoRoot),
|
|
@@ -5429,7 +6089,7 @@ function createApiServer(root, options = {}) {
|
|
|
5429
6089
|
created: task.created,
|
|
5430
6090
|
updated: task.updated,
|
|
5431
6091
|
body: parsed.body,
|
|
5432
|
-
file_path:
|
|
6092
|
+
file_path: path21.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
5433
6093
|
}
|
|
5434
6094
|
});
|
|
5435
6095
|
return;
|
|
@@ -5517,9 +6177,9 @@ function registerServeCommand(program) {
|
|
|
5517
6177
|
}
|
|
5518
6178
|
|
|
5519
6179
|
// src/commands/show.ts
|
|
5520
|
-
import
|
|
5521
|
-
import
|
|
5522
|
-
import { parseIdeaFile as
|
|
6180
|
+
import fs18 from "fs";
|
|
6181
|
+
import path22 from "path";
|
|
6182
|
+
import { parseIdeaFile as parseIdeaFile7 } from "@kitsy/coop-core";
|
|
5523
6183
|
function stringify(value) {
|
|
5524
6184
|
if (value === null || value === void 0) return "-";
|
|
5525
6185
|
if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "-";
|
|
@@ -5537,13 +6197,13 @@ function pushListSection(lines, title, values) {
|
|
|
5537
6197
|
}
|
|
5538
6198
|
}
|
|
5539
6199
|
function loadComputedFromIndex(root, taskId) {
|
|
5540
|
-
const indexPath =
|
|
5541
|
-
if (!
|
|
6200
|
+
const indexPath = path22.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
6201
|
+
if (!fs18.existsSync(indexPath)) {
|
|
5542
6202
|
return null;
|
|
5543
6203
|
}
|
|
5544
6204
|
let parsed;
|
|
5545
6205
|
try {
|
|
5546
|
-
parsed = JSON.parse(
|
|
6206
|
+
parsed = JSON.parse(fs18.readFileSync(indexPath, "utf8"));
|
|
5547
6207
|
} catch {
|
|
5548
6208
|
return null;
|
|
5549
6209
|
}
|
|
@@ -5578,12 +6238,16 @@ function loadComputedFromIndex(root, taskId) {
|
|
|
5578
6238
|
}
|
|
5579
6239
|
return null;
|
|
5580
6240
|
}
|
|
6241
|
+
function formatTimeSummary(task) {
|
|
6242
|
+
const planned = task.time?.planned_hours;
|
|
6243
|
+
const worked = (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((sum, entry) => sum + entry.hours, 0);
|
|
6244
|
+
const plannedLogged = (task.time?.logs ?? []).filter((entry) => entry.kind === "planned").reduce((sum, entry) => sum + entry.hours, 0);
|
|
6245
|
+
return `planned_hours=${planned ?? "-"} | planned_logged=${plannedLogged || 0} | worked=${worked || 0}`;
|
|
6246
|
+
}
|
|
5581
6247
|
function showTask(taskId) {
|
|
5582
6248
|
const root = resolveRepoRoot();
|
|
5583
|
-
const
|
|
5584
|
-
const
|
|
5585
|
-
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5586
|
-
const parsed = parseTaskFile13(taskFile);
|
|
6249
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6250
|
+
const { filePath, parsed } = loadTaskEntry(root, taskId);
|
|
5587
6251
|
const task = parsed.task;
|
|
5588
6252
|
const body = parsed.body.trim();
|
|
5589
6253
|
const computed = loadComputedFromIndex(root, task.id);
|
|
@@ -5593,15 +6257,22 @@ function showTask(taskId) {
|
|
|
5593
6257
|
`Status: ${task.status}`,
|
|
5594
6258
|
`Type: ${task.type}`,
|
|
5595
6259
|
`Priority: ${task.priority ?? "-"}`,
|
|
6260
|
+
`Effective Priority: ${taskEffectivePriority(task, context.track)}`,
|
|
5596
6261
|
`Track: ${task.track ?? "-"}`,
|
|
6262
|
+
`Delivery Tracks: ${stringify(task.delivery_tracks)}`,
|
|
6263
|
+
`Priority Context: ${stringify(task.priority_context)}`,
|
|
5597
6264
|
`Assignee: ${task.assignee ?? "-"}`,
|
|
5598
6265
|
`Delivery: ${task.delivery ?? "-"}`,
|
|
6266
|
+
`Fix Versions: ${stringify(task.fix_versions)}`,
|
|
6267
|
+
`Released In: ${stringify(task.released_in)}`,
|
|
6268
|
+
`Story Points: ${task.story_points ?? "-"}`,
|
|
6269
|
+
`Time: ${formatTimeSummary(task)}`,
|
|
5599
6270
|
`Aliases: ${stringify(task.aliases)}`,
|
|
5600
6271
|
`Depends On: ${stringify(task.depends_on)}`,
|
|
5601
6272
|
`Tags: ${stringify(task.tags)}`,
|
|
5602
6273
|
`Created: ${task.created}`,
|
|
5603
6274
|
`Updated: ${task.updated}`,
|
|
5604
|
-
`File: ${
|
|
6275
|
+
`File: ${path22.relative(root, filePath)}`,
|
|
5605
6276
|
""
|
|
5606
6277
|
];
|
|
5607
6278
|
pushListSection(lines, "Acceptance", task.acceptance);
|
|
@@ -5618,15 +6289,25 @@ function showTask(taskId) {
|
|
|
5618
6289
|
lines.push(`- Promoted To: ${stringify(task.origin.promoted_to)}`);
|
|
5619
6290
|
lines.push(`- Snapshot SHA256: ${task.origin.snapshot_sha256 ?? "-"}`);
|
|
5620
6291
|
}
|
|
5621
|
-
lines.push(
|
|
5622
|
-
|
|
5623
|
-
"
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
6292
|
+
lines.push("", "Comments:");
|
|
6293
|
+
if (!task.comments || task.comments.length === 0) {
|
|
6294
|
+
lines.push("- none");
|
|
6295
|
+
} else {
|
|
6296
|
+
for (const comment of task.comments) {
|
|
6297
|
+
lines.push(`- ${comment.at} ${comment.author}: ${comment.body}`);
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
lines.push("", "Time Logs:");
|
|
6301
|
+
if (!task.time?.logs || task.time.logs.length === 0) {
|
|
6302
|
+
lines.push("- none");
|
|
6303
|
+
} else {
|
|
6304
|
+
for (const entry of task.time.logs) {
|
|
6305
|
+
lines.push(`- ${entry.at} ${entry.actor} ${entry.kind} ${entry.hours}h${entry.note ? ` (${entry.note})` : ""}`);
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
lines.push("", "Body:", body || "-", "", "Computed:");
|
|
5628
6309
|
if (!computed) {
|
|
5629
|
-
lines.push(`index not built (${
|
|
6310
|
+
lines.push(`index not built (${path22.relative(root, path22.join(ensureCoopInitialized(root), ".index", "tasks.json"))} missing)`);
|
|
5630
6311
|
} else {
|
|
5631
6312
|
for (const [key, value] of Object.entries(computed)) {
|
|
5632
6313
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -5636,10 +6317,9 @@ function showTask(taskId) {
|
|
|
5636
6317
|
}
|
|
5637
6318
|
function showIdea(ideaId) {
|
|
5638
6319
|
const root = resolveRepoRoot();
|
|
5639
|
-
ensureCoopInitialized(root);
|
|
5640
6320
|
const target = resolveReference(root, ideaId, "idea");
|
|
5641
|
-
const ideaFile =
|
|
5642
|
-
const parsed =
|
|
6321
|
+
const ideaFile = path22.join(root, ...target.file.split("/"));
|
|
6322
|
+
const parsed = parseIdeaFile7(ideaFile);
|
|
5643
6323
|
const idea = parsed.idea;
|
|
5644
6324
|
const body = parsed.body.trim();
|
|
5645
6325
|
const lines = [
|
|
@@ -5652,29 +6332,68 @@ function showIdea(ideaId) {
|
|
|
5652
6332
|
`Tags: ${stringify(idea.tags)}`,
|
|
5653
6333
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
5654
6334
|
`Created: ${idea.created}`,
|
|
5655
|
-
`File: ${
|
|
6335
|
+
`File: ${path22.relative(root, ideaFile)}`,
|
|
5656
6336
|
"",
|
|
5657
6337
|
"Body:",
|
|
5658
6338
|
body || "-"
|
|
5659
6339
|
];
|
|
5660
6340
|
console.log(lines.join("\n"));
|
|
5661
6341
|
}
|
|
6342
|
+
function showDelivery(ref) {
|
|
6343
|
+
const root = resolveRepoRoot();
|
|
6344
|
+
const { filePath, delivery, body } = resolveDeliveryEntry(root, ref);
|
|
6345
|
+
const lines = [
|
|
6346
|
+
`Delivery: ${delivery.id}`,
|
|
6347
|
+
`Name: ${delivery.name}`,
|
|
6348
|
+
`Status: ${delivery.status}`,
|
|
6349
|
+
`Target Date: ${delivery.target_date ?? "-"}`,
|
|
6350
|
+
`Started Date: ${delivery.started_date ?? "-"}`,
|
|
6351
|
+
`Delivered Date: ${delivery.delivered_date ?? "-"}`,
|
|
6352
|
+
`Capacity Profiles: ${delivery.capacity_profiles.length > 0 ? delivery.capacity_profiles.join(", ") : "-"}`,
|
|
6353
|
+
`Scope Include: ${delivery.scope.include.length > 0 ? delivery.scope.include.join(", ") : "-"}`,
|
|
6354
|
+
`Scope Exclude: ${delivery.scope.exclude.length > 0 ? delivery.scope.exclude.join(", ") : "-"}`,
|
|
6355
|
+
`File: ${path22.relative(root, filePath)}`,
|
|
6356
|
+
"",
|
|
6357
|
+
"Body:",
|
|
6358
|
+
body.trim() || "-"
|
|
6359
|
+
];
|
|
6360
|
+
console.log(lines.join("\n"));
|
|
6361
|
+
}
|
|
6362
|
+
function showByReference(ref) {
|
|
6363
|
+
const root = resolveRepoRoot();
|
|
6364
|
+
try {
|
|
6365
|
+
const resolved = resolveReference(root, ref);
|
|
6366
|
+
if (resolved.type === "task") {
|
|
6367
|
+
showTask(ref);
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
showIdea(ref);
|
|
6371
|
+
return;
|
|
6372
|
+
} catch {
|
|
6373
|
+
showDelivery(ref);
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
5662
6376
|
function registerShowCommand(program) {
|
|
5663
|
-
const show = program.command("show").description("Show detailed COOP entities")
|
|
6377
|
+
const show = program.command("show").description("Show detailed COOP entities").argument("[ref]", "Task, idea, or delivery reference").action((ref) => {
|
|
6378
|
+
if (!ref?.trim()) {
|
|
6379
|
+
throw new Error("Provide a task, idea, or delivery reference.");
|
|
6380
|
+
}
|
|
6381
|
+
showByReference(ref);
|
|
6382
|
+
});
|
|
5664
6383
|
show.command("task").description("Show task details").argument("<id>", "Task ID").action((id) => {
|
|
5665
6384
|
showTask(id);
|
|
5666
6385
|
});
|
|
5667
6386
|
show.command("idea").description("Show idea details").argument("<id>", "Idea ID").action((id) => {
|
|
5668
6387
|
showIdea(id);
|
|
5669
6388
|
});
|
|
5670
|
-
show.command("delivery").description("Show delivery details
|
|
5671
|
-
|
|
6389
|
+
show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").action((id) => {
|
|
6390
|
+
showDelivery(id);
|
|
5672
6391
|
});
|
|
5673
6392
|
}
|
|
5674
6393
|
|
|
5675
6394
|
// src/commands/status.ts
|
|
5676
6395
|
import chalk4 from "chalk";
|
|
5677
|
-
import { TaskStatus as TaskStatus4, analyze_feasibility as analyze_feasibility3, load_graph as
|
|
6396
|
+
import { TaskStatus as TaskStatus4, analyze_feasibility as analyze_feasibility3, load_graph as load_graph11 } from "@kitsy/coop-core";
|
|
5678
6397
|
function countBy(values, keyFn) {
|
|
5679
6398
|
const out = /* @__PURE__ */ new Map();
|
|
5680
6399
|
for (const value of values) {
|
|
@@ -5695,10 +6414,16 @@ function completionSummary(done, totalTasks) {
|
|
|
5695
6414
|
const percent = totalTasks > 0 ? Math.round(done / totalTasks * 100) : 0;
|
|
5696
6415
|
return `${done}/${totalTasks} (${percent}%)`;
|
|
5697
6416
|
}
|
|
6417
|
+
function workedHours(tasks) {
|
|
6418
|
+
return tasks.reduce(
|
|
6419
|
+
(sum, task) => sum + (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((entrySum, entry) => entrySum + entry.hours, 0),
|
|
6420
|
+
0
|
|
6421
|
+
);
|
|
6422
|
+
}
|
|
5698
6423
|
function registerStatusCommand(program) {
|
|
5699
6424
|
program.command("status").description("Project dashboard overview").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
|
|
5700
6425
|
const root = resolveRepoRoot();
|
|
5701
|
-
const graph =
|
|
6426
|
+
const graph = load_graph11(coopDir(root));
|
|
5702
6427
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
5703
6428
|
const tasks = Array.from(graph.nodes.values());
|
|
5704
6429
|
const tasksByStatus = countBy(tasks, (task) => task.status);
|
|
@@ -5718,6 +6443,14 @@ function registerStatusCommand(program) {
|
|
|
5718
6443
|
const trackRows = Array.from(tasksByTrack.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([track, count]) => [track, String(count)]);
|
|
5719
6444
|
lines.push(formatTable(["Track", "Count"], trackRows));
|
|
5720
6445
|
lines.push("");
|
|
6446
|
+
const totalStoryPoints = tasks.reduce((sum, task) => sum + (task.story_points ?? 0), 0);
|
|
6447
|
+
const totalPlannedHours = tasks.reduce((sum, task) => sum + (task.time?.planned_hours ?? 0), 0);
|
|
6448
|
+
const totalWorkedHours = workedHours(tasks);
|
|
6449
|
+
lines.push(chalk4.bold("Effort Snapshot"));
|
|
6450
|
+
lines.push(`Story points: ${totalStoryPoints}`);
|
|
6451
|
+
lines.push(`Planned hours: ${totalPlannedHours.toFixed(2)}`);
|
|
6452
|
+
lines.push(`Worked hours: ${totalWorkedHours.toFixed(2)}`);
|
|
6453
|
+
lines.push("");
|
|
5721
6454
|
lines.push(chalk4.bold("Delivery Health"));
|
|
5722
6455
|
const deliveryRows = [];
|
|
5723
6456
|
const riskLines = [];
|
|
@@ -5769,8 +6502,8 @@ function registerTransitionCommand(program) {
|
|
|
5769
6502
|
}
|
|
5770
6503
|
|
|
5771
6504
|
// src/commands/taskflow.ts
|
|
5772
|
-
import
|
|
5773
|
-
import { parseTaskFile as
|
|
6505
|
+
import path23 from "path";
|
|
6506
|
+
import { parseTaskFile as parseTaskFile15 } from "@kitsy/coop-core";
|
|
5774
6507
|
function normalizeExecutor(value) {
|
|
5775
6508
|
if (!value?.trim()) return void 0;
|
|
5776
6509
|
const normalized = value.trim().toLowerCase();
|
|
@@ -5807,8 +6540,8 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5807
6540
|
}
|
|
5808
6541
|
}
|
|
5809
6542
|
const reference = resolveReference(root, taskId, "task");
|
|
5810
|
-
const filePath =
|
|
5811
|
-
const parsed =
|
|
6543
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
6544
|
+
const parsed = parseTaskFile15(filePath);
|
|
5812
6545
|
if (parsed.task.status === "in_progress") {
|
|
5813
6546
|
console.log(`Task ${parsed.task.id} is already in_progress.`);
|
|
5814
6547
|
return;
|
|
@@ -5820,10 +6553,37 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5820
6553
|
});
|
|
5821
6554
|
console.log(`Updated ${transitioned.task.id}: ${transitioned.from} -> ${transitioned.to}`);
|
|
5822
6555
|
}
|
|
6556
|
+
function maybePromote(root, taskId, options) {
|
|
6557
|
+
if (!options.promote) {
|
|
6558
|
+
return;
|
|
6559
|
+
}
|
|
6560
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6561
|
+
const { filePath, parsed } = loadTaskEntry(root, taskId);
|
|
6562
|
+
const promoted = promoteTaskForContext(parsed.task, {
|
|
6563
|
+
track: options.track ?? context.track,
|
|
6564
|
+
version: options.version ?? context.version
|
|
6565
|
+
});
|
|
6566
|
+
writeTaskEntry(filePath, parsed, promoted);
|
|
6567
|
+
console.log(`Promoted ${promoted.id}`);
|
|
6568
|
+
}
|
|
6569
|
+
function printResolvedSelectionContext(root, options) {
|
|
6570
|
+
if (!isVerboseRequested()) {
|
|
6571
|
+
return;
|
|
6572
|
+
}
|
|
6573
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6574
|
+
for (const line of formatResolvedContextMessage({
|
|
6575
|
+
track: resolveContextValueWithSource(options.track, context.track, sharedDefault(root, "track")),
|
|
6576
|
+
delivery: resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery")),
|
|
6577
|
+
version: resolveContextValueWithSource(options.version, context.version, sharedDefault(root, "version"))
|
|
6578
|
+
})) {
|
|
6579
|
+
console.log(line);
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
5823
6582
|
function registerTaskFlowCommands(program) {
|
|
5824
6583
|
const next = program.command("next").description("Select the next COOP work item");
|
|
5825
6584
|
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) => {
|
|
5826
6585
|
const root = resolveRepoRoot();
|
|
6586
|
+
printResolvedSelectionContext(root, options);
|
|
5827
6587
|
const selected = selectTopReadyTask(root, {
|
|
5828
6588
|
track: options.track,
|
|
5829
6589
|
delivery: options.delivery,
|
|
@@ -5833,29 +6593,49 @@ function registerTaskFlowCommands(program) {
|
|
|
5833
6593
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
5834
6594
|
});
|
|
5835
6595
|
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) => {
|
|
6596
|
+
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 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("--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
6597
|
const root = resolveRepoRoot();
|
|
5838
|
-
|
|
6598
|
+
printResolvedSelectionContext(root, options);
|
|
6599
|
+
const selected = id?.trim() ? {
|
|
6600
|
+
entry: {
|
|
6601
|
+
task: loadTaskEntry(root, id).parsed.task,
|
|
6602
|
+
score: 0,
|
|
6603
|
+
readiness: statusToReadiness(loadTaskEntry(root, id).parsed.task.status),
|
|
6604
|
+
fits_capacity: true,
|
|
6605
|
+
fits_wip: true
|
|
6606
|
+
},
|
|
6607
|
+
selection: {
|
|
6608
|
+
track: options.track,
|
|
6609
|
+
delivery: options.delivery,
|
|
6610
|
+
version: options.version,
|
|
6611
|
+
executor: normalizeExecutor(options.executor),
|
|
6612
|
+
today: options.today
|
|
6613
|
+
}
|
|
6614
|
+
} : selectTopReadyTask(root, {
|
|
5839
6615
|
track: options.track,
|
|
5840
6616
|
delivery: options.delivery,
|
|
6617
|
+
version: options.version,
|
|
5841
6618
|
executor: normalizeExecutor(options.executor),
|
|
5842
6619
|
today: options.today
|
|
5843
6620
|
});
|
|
5844
6621
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
6622
|
+
maybePromote(root, selected.entry.task.id, options);
|
|
5845
6623
|
await claimAndStart(root, selected.entry.task.id, options);
|
|
5846
6624
|
});
|
|
5847
6625
|
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) => {
|
|
6626
|
+
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("--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
6627
|
const root = resolveRepoRoot();
|
|
6628
|
+
printResolvedSelectionContext(root, options);
|
|
5850
6629
|
const taskId = id?.trim() || selectTopReadyTask(root, {
|
|
5851
6630
|
track: options.track,
|
|
5852
6631
|
delivery: options.delivery,
|
|
6632
|
+
version: options.version,
|
|
5853
6633
|
executor: normalizeExecutor(options.executor),
|
|
5854
6634
|
today: options.today
|
|
5855
6635
|
}).entry.task.id;
|
|
5856
6636
|
const reference = resolveReference(root, taskId, "task");
|
|
5857
|
-
const filePath =
|
|
5858
|
-
const parsed =
|
|
6637
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
6638
|
+
const parsed = parseTaskFile15(filePath);
|
|
5859
6639
|
console.log(
|
|
5860
6640
|
formatSelectedTask(
|
|
5861
6641
|
{
|
|
@@ -5868,29 +6648,31 @@ function registerTaskFlowCommands(program) {
|
|
|
5868
6648
|
{
|
|
5869
6649
|
track: options.track,
|
|
5870
6650
|
delivery: options.delivery,
|
|
6651
|
+
version: options.version,
|
|
5871
6652
|
executor: normalizeExecutor(options.executor),
|
|
5872
6653
|
today: options.today
|
|
5873
6654
|
}
|
|
5874
6655
|
)
|
|
5875
6656
|
);
|
|
6657
|
+
maybePromote(root, reference.id, options);
|
|
5876
6658
|
await claimAndStart(root, reference.id, options);
|
|
5877
6659
|
});
|
|
5878
6660
|
}
|
|
5879
6661
|
|
|
5880
6662
|
// src/commands/ui.ts
|
|
5881
|
-
import
|
|
5882
|
-
import
|
|
6663
|
+
import fs19 from "fs";
|
|
6664
|
+
import path24 from "path";
|
|
5883
6665
|
import { createRequire } from "module";
|
|
5884
6666
|
import { fileURLToPath } from "url";
|
|
5885
6667
|
import { spawn } from "child_process";
|
|
5886
6668
|
import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
|
|
5887
6669
|
function findPackageRoot(entryPath) {
|
|
5888
|
-
let current =
|
|
6670
|
+
let current = path24.dirname(entryPath);
|
|
5889
6671
|
while (true) {
|
|
5890
|
-
if (
|
|
6672
|
+
if (fs19.existsSync(path24.join(current, "package.json"))) {
|
|
5891
6673
|
return current;
|
|
5892
6674
|
}
|
|
5893
|
-
const parent =
|
|
6675
|
+
const parent = path24.dirname(current);
|
|
5894
6676
|
if (parent === current) {
|
|
5895
6677
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
5896
6678
|
}
|
|
@@ -5939,9 +6721,9 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
5939
6721
|
const project = resolveProject(repoRoot);
|
|
5940
6722
|
ensureIndex(repoRoot);
|
|
5941
6723
|
const uiRoot = resolveUiPackageRoot();
|
|
5942
|
-
const requireFromUi = createRequire(
|
|
6724
|
+
const requireFromUi = createRequire(path24.join(uiRoot, "package.json"));
|
|
5943
6725
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
5944
|
-
const viteBin =
|
|
6726
|
+
const viteBin = path24.join(path24.dirname(vitePackageJson), "bin", "vite.js");
|
|
5945
6727
|
const url = `http://${host}:${port}`;
|
|
5946
6728
|
console.log(`COOP UI: ${url}`);
|
|
5947
6729
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -5987,12 +6769,197 @@ function registerUiCommand(program) {
|
|
|
5987
6769
|
});
|
|
5988
6770
|
}
|
|
5989
6771
|
|
|
6772
|
+
// src/commands/update.ts
|
|
6773
|
+
import fs20 from "fs";
|
|
6774
|
+
import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as TaskStatus5, stringifyFrontmatter as stringifyFrontmatter5 } from "@kitsy/coop-core";
|
|
6775
|
+
function collect(value, previous = []) {
|
|
6776
|
+
return [...previous, value];
|
|
6777
|
+
}
|
|
6778
|
+
function unique2(items) {
|
|
6779
|
+
return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
|
|
6780
|
+
}
|
|
6781
|
+
function removeValues(source, values) {
|
|
6782
|
+
if (!source) return source;
|
|
6783
|
+
const removals = new Set((values ?? []).map((value) => value.trim()));
|
|
6784
|
+
const next = source.filter((entry) => !removals.has(entry));
|
|
6785
|
+
return next.length > 0 ? next : void 0;
|
|
6786
|
+
}
|
|
6787
|
+
function addValues(source, values) {
|
|
6788
|
+
const next = unique2([...source ?? [], ...values ?? []]);
|
|
6789
|
+
return next.length > 0 ? next : void 0;
|
|
6790
|
+
}
|
|
6791
|
+
function loadBody(options) {
|
|
6792
|
+
if (options.bodyFile) {
|
|
6793
|
+
return fs20.readFileSync(options.bodyFile, "utf8");
|
|
6794
|
+
}
|
|
6795
|
+
if (options.bodyStdin) {
|
|
6796
|
+
return fs20.readFileSync(0, "utf8");
|
|
6797
|
+
}
|
|
6798
|
+
return void 0;
|
|
6799
|
+
}
|
|
6800
|
+
function applyTrackPriorityOverrides(task, instructions) {
|
|
6801
|
+
if (!instructions || instructions.length === 0) {
|
|
6802
|
+
return task;
|
|
6803
|
+
}
|
|
6804
|
+
const next = { ...task.priority_context ?? {} };
|
|
6805
|
+
for (const instruction of instructions) {
|
|
6806
|
+
const [track, priority] = instruction.split(":", 2).map((part) => part?.trim() ?? "");
|
|
6807
|
+
if (!track || !priority) {
|
|
6808
|
+
throw new Error(`Invalid --priority-in value '${instruction}'. Expected <track>:<priority>.`);
|
|
6809
|
+
}
|
|
6810
|
+
if (!Object.values(TaskPriority3).includes(priority)) {
|
|
6811
|
+
throw new Error(`Invalid priority '${priority}'. Expected one of ${Object.values(TaskPriority3).join(", ")}.`);
|
|
6812
|
+
}
|
|
6813
|
+
next[track] = priority;
|
|
6814
|
+
}
|
|
6815
|
+
return { ...task, priority_context: Object.keys(next).length > 0 ? next : void 0 };
|
|
6816
|
+
}
|
|
6817
|
+
function clearTrackPriorityOverrides(task, tracks) {
|
|
6818
|
+
if (!tracks || tracks.length === 0 || !task.priority_context) {
|
|
6819
|
+
return task;
|
|
6820
|
+
}
|
|
6821
|
+
const next = { ...task.priority_context };
|
|
6822
|
+
for (const track of tracks) {
|
|
6823
|
+
delete next[track];
|
|
6824
|
+
}
|
|
6825
|
+
return { ...task, priority_context: Object.keys(next).length > 0 ? next : void 0 };
|
|
6826
|
+
}
|
|
6827
|
+
function normalizeTaskStatus(status) {
|
|
6828
|
+
const value = status.trim().toLowerCase();
|
|
6829
|
+
if (!Object.values(TaskStatus5).includes(value)) {
|
|
6830
|
+
throw new Error(`Invalid status '${status}'. Expected one of ${Object.values(TaskStatus5).join(", ")}.`);
|
|
6831
|
+
}
|
|
6832
|
+
return value;
|
|
6833
|
+
}
|
|
6834
|
+
function normalizeTaskPriority(priority) {
|
|
6835
|
+
const value = priority.trim().toLowerCase();
|
|
6836
|
+
if (!Object.values(TaskPriority3).includes(value)) {
|
|
6837
|
+
throw new Error(`Invalid priority '${priority}'. Expected one of ${Object.values(TaskPriority3).join(", ")}.`);
|
|
6838
|
+
}
|
|
6839
|
+
return value;
|
|
6840
|
+
}
|
|
6841
|
+
function renderTaskPreview(task, body) {
|
|
6842
|
+
return stringifyFrontmatter5(task, body);
|
|
6843
|
+
}
|
|
6844
|
+
function updateTask(id, options) {
|
|
6845
|
+
const root = resolveRepoRoot();
|
|
6846
|
+
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
6847
|
+
let next = {
|
|
6848
|
+
...parsed.task,
|
|
6849
|
+
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
6850
|
+
};
|
|
6851
|
+
if (options.title) next.title = options.title.trim();
|
|
6852
|
+
if (options.priority) next.priority = normalizeTaskPriority(options.priority);
|
|
6853
|
+
if (options.status) next.status = normalizeTaskStatus(options.status);
|
|
6854
|
+
if (options.assign !== void 0) next.assignee = options.assign.trim() || null;
|
|
6855
|
+
if (options.track) next.track = options.track.trim();
|
|
6856
|
+
if (options.delivery !== void 0) next.delivery = options.delivery.trim() || null;
|
|
6857
|
+
if (options.storyPoints !== void 0) next.story_points = Number(options.storyPoints);
|
|
6858
|
+
if (options.plannedHours !== void 0) next = setTaskPlannedHours(next, Number(options.plannedHours));
|
|
6859
|
+
next = {
|
|
6860
|
+
...next,
|
|
6861
|
+
delivery_tracks: addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
|
|
6862
|
+
depends_on: addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
|
|
6863
|
+
tags: addValues(removeValues(next.tags, options.removeTag), options.addTag),
|
|
6864
|
+
fix_versions: addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
|
|
6865
|
+
released_in: addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
|
|
6866
|
+
acceptance: addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
|
|
6867
|
+
tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
|
|
6868
|
+
};
|
|
6869
|
+
next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
|
|
6870
|
+
validateTaskForWrite(next, filePath);
|
|
6871
|
+
const nextBody = loadBody(options) ?? parsed.body;
|
|
6872
|
+
if (options.dryRun) {
|
|
6873
|
+
console.log(renderTaskPreview(next, nextBody).trimEnd());
|
|
6874
|
+
return;
|
|
6875
|
+
}
|
|
6876
|
+
writeTaskEntry(filePath, parsed, next, nextBody);
|
|
6877
|
+
console.log(`Updated ${next.id}`);
|
|
6878
|
+
}
|
|
6879
|
+
function updateIdea(id, options) {
|
|
6880
|
+
const root = resolveRepoRoot();
|
|
6881
|
+
const { filePath, parsed } = loadIdeaEntry(root, id);
|
|
6882
|
+
const nextStatus = options.status?.trim().toLowerCase();
|
|
6883
|
+
if (nextStatus && !Object.values(IdeaStatus3).includes(nextStatus)) {
|
|
6884
|
+
throw new Error(`Invalid idea status '${options.status}'. Expected one of ${Object.values(IdeaStatus3).join(", ")}.`);
|
|
6885
|
+
}
|
|
6886
|
+
const next = {
|
|
6887
|
+
...parsed.idea,
|
|
6888
|
+
title: options.title?.trim() || parsed.idea.title,
|
|
6889
|
+
status: nextStatus || parsed.idea.status,
|
|
6890
|
+
tags: addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag) ?? [],
|
|
6891
|
+
linked_tasks: addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask) ?? []
|
|
6892
|
+
};
|
|
6893
|
+
const nextBody = loadBody(options) ?? parsed.body;
|
|
6894
|
+
if (options.dryRun) {
|
|
6895
|
+
console.log(stringifyFrontmatter5(next, nextBody).trimEnd());
|
|
6896
|
+
return;
|
|
6897
|
+
}
|
|
6898
|
+
writeIdeaFile(filePath, parsed, next, nextBody);
|
|
6899
|
+
console.log(`Updated ${next.id}`);
|
|
6900
|
+
}
|
|
6901
|
+
function registerUpdateCommand(program) {
|
|
6902
|
+
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>").option("--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) => {
|
|
6903
|
+
let resolved = resolveOptionalEntityArg(first, second, ["task", "idea"], "task");
|
|
6904
|
+
if (!second && resolved.entity === "task") {
|
|
6905
|
+
try {
|
|
6906
|
+
updateTask(resolved.id, options);
|
|
6907
|
+
return;
|
|
6908
|
+
} catch (error) {
|
|
6909
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6910
|
+
if (!message.includes("not found")) {
|
|
6911
|
+
throw error;
|
|
6912
|
+
}
|
|
6913
|
+
resolved = { entity: "idea", id: first.trim() };
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
if (resolved.entity === "idea") {
|
|
6917
|
+
updateIdea(resolved.id, options);
|
|
6918
|
+
return;
|
|
6919
|
+
}
|
|
6920
|
+
updateTask(resolved.id, options);
|
|
6921
|
+
});
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
// src/commands/use.ts
|
|
6925
|
+
function printContext(values) {
|
|
6926
|
+
console.log(`Track: ${values.track ?? "-"}`);
|
|
6927
|
+
console.log(`Delivery: ${values.delivery ?? "-"}`);
|
|
6928
|
+
console.log(`Version: ${values.version ?? "-"}`);
|
|
6929
|
+
}
|
|
6930
|
+
function registerUseCommand(program) {
|
|
6931
|
+
const use = program.command("use").description("Manage user-local working defaults for the current COOP project");
|
|
6932
|
+
use.command("show").description("Show the current working context").action(() => {
|
|
6933
|
+
const root = resolveRepoRoot();
|
|
6934
|
+
printContext(readWorkingContext(root, resolveCoopHome()));
|
|
6935
|
+
});
|
|
6936
|
+
use.command("track").description("Set the default working track").argument("<id>", "Track id").action((id) => {
|
|
6937
|
+
const root = resolveRepoRoot();
|
|
6938
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { track: id }));
|
|
6939
|
+
});
|
|
6940
|
+
use.command("delivery").description("Set the default working delivery").argument("<id>", "Delivery id").action((id) => {
|
|
6941
|
+
const root = resolveRepoRoot();
|
|
6942
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: id }));
|
|
6943
|
+
});
|
|
6944
|
+
use.command("version").description("Set the default working version").argument("<id>", "Version label").action((id) => {
|
|
6945
|
+
const root = resolveRepoRoot();
|
|
6946
|
+
printContext(updateWorkingContext(root, resolveCoopHome(), { version: id }));
|
|
6947
|
+
});
|
|
6948
|
+
use.command("clear").description("Clear one working-context key or all of them").argument("[scope]", "track|delivery|version|all", "all").action((scope) => {
|
|
6949
|
+
if (scope !== "track" && scope !== "delivery" && scope !== "version" && scope !== "all") {
|
|
6950
|
+
throw new Error(`Invalid scope '${scope}'. Expected track|delivery|version|all.`);
|
|
6951
|
+
}
|
|
6952
|
+
const root = resolveRepoRoot();
|
|
6953
|
+
printContext(clearWorkingContext(root, resolveCoopHome(), scope));
|
|
6954
|
+
});
|
|
6955
|
+
}
|
|
6956
|
+
|
|
5990
6957
|
// src/commands/view.ts
|
|
5991
6958
|
import {
|
|
5992
6959
|
analyze_feasibility as analyze_feasibility4,
|
|
5993
6960
|
compute_velocity,
|
|
5994
6961
|
load_completed_runs,
|
|
5995
|
-
load_graph as
|
|
6962
|
+
load_graph as load_graph12,
|
|
5996
6963
|
simulate_schedule
|
|
5997
6964
|
} from "@kitsy/coop-core";
|
|
5998
6965
|
var STATUS_COLUMNS = [
|
|
@@ -6055,9 +7022,12 @@ function formatAccuracy(value) {
|
|
|
6055
7022
|
if (value == null) return "-";
|
|
6056
7023
|
return `${Math.round(value * 100)}%`;
|
|
6057
7024
|
}
|
|
7025
|
+
function workedHours2(task) {
|
|
7026
|
+
return (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((sum, entry) => sum + entry.hours, 0);
|
|
7027
|
+
}
|
|
6058
7028
|
function runKanban() {
|
|
6059
7029
|
const root = resolveRepoRoot();
|
|
6060
|
-
const graph =
|
|
7030
|
+
const graph = load_graph12(coopDir(root));
|
|
6061
7031
|
const grouped = /* @__PURE__ */ new Map();
|
|
6062
7032
|
for (const status of STATUS_COLUMNS) {
|
|
6063
7033
|
grouped.set(status, []);
|
|
@@ -6089,11 +7059,19 @@ function runKanban() {
|
|
|
6089
7059
|
}
|
|
6090
7060
|
function runTimeline(options) {
|
|
6091
7061
|
const root = resolveRepoRoot();
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
|
|
7062
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
7063
|
+
const graph = load_graph12(coopDir(root));
|
|
7064
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
7065
|
+
if (isVerboseRequested()) {
|
|
7066
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
7067
|
+
console.log(line);
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
const deliveryRef = resolvedDelivery.value;
|
|
7071
|
+
if (!deliveryRef) {
|
|
7072
|
+
throw new Error("delivery is not set; pass --delivery <name> or set a default working delivery with `coop use delivery <id>`.");
|
|
6095
7073
|
}
|
|
6096
|
-
const delivery = resolveDelivery(graph,
|
|
7074
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
6097
7075
|
const include = new Set(delivery.scope.include);
|
|
6098
7076
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
6099
7077
|
const tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -6134,7 +7112,7 @@ function runTimeline(options) {
|
|
|
6134
7112
|
function runVelocity(options) {
|
|
6135
7113
|
const root = resolveRepoRoot();
|
|
6136
7114
|
const coopPath = coopDir(root);
|
|
6137
|
-
const graph =
|
|
7115
|
+
const graph = load_graph12(coopPath);
|
|
6138
7116
|
const config = readCoopConfig(root).raw;
|
|
6139
7117
|
const scheduling = typeof config.scheduling === "object" && config.scheduling !== null ? config.scheduling : {};
|
|
6140
7118
|
const configuredWeeks = Number(scheduling.velocity_window_weeks);
|
|
@@ -6144,12 +7122,19 @@ function runVelocity(options) {
|
|
|
6144
7122
|
today: options.today ?? /* @__PURE__ */ new Date(),
|
|
6145
7123
|
graph
|
|
6146
7124
|
});
|
|
7125
|
+
const completedTaskIds = new Set(runs.map((run) => run.task));
|
|
7126
|
+
const completedTasks = Array.from(completedTaskIds).map((taskId) => graph.nodes.get(taskId)).filter((task) => Boolean(task));
|
|
7127
|
+
const storyPointsTotal = completedTasks.reduce((sum, task) => sum + (task.story_points ?? 0), 0);
|
|
7128
|
+
const plannedHoursTotal = completedTasks.reduce((sum, task) => sum + (task.time?.planned_hours ?? 0), 0);
|
|
7129
|
+
const workedHoursTotal = completedTasks.reduce((sum, task) => sum + workedHours2(task), 0);
|
|
6147
7130
|
console.log(`Velocity: last ${metrics.window_weeks} weeks`);
|
|
6148
7131
|
console.log(`Trend: ${metrics.trend}`);
|
|
6149
7132
|
console.log(`Completed runs: ${metrics.completed_runs}`);
|
|
6150
7133
|
console.log(`Tasks/week: ${metrics.tasks_completed_per_week}`);
|
|
6151
7134
|
console.log(`Hours/week: ${metrics.hours_delivered_per_week}`);
|
|
7135
|
+
console.log(`Story points/week: ${Number((storyPointsTotal / metrics.window_weeks).toFixed(2))}`);
|
|
6152
7136
|
console.log(`Accuracy: ${formatAccuracy(metrics.accuracy_ratio)}`);
|
|
7137
|
+
console.log(`Planned vs Worked: ${plannedHoursTotal.toFixed(2)}h planned | ${workedHoursTotal.toFixed(2)}h worked`);
|
|
6153
7138
|
console.log(`Sparkline: ${sparkline(metrics.points.map((point) => point.completed_tasks))}`);
|
|
6154
7139
|
console.log("");
|
|
6155
7140
|
console.log(
|
|
@@ -6165,12 +7150,20 @@ function runVelocity(options) {
|
|
|
6165
7150
|
}
|
|
6166
7151
|
function runBurndown(options) {
|
|
6167
7152
|
const root = resolveRepoRoot();
|
|
7153
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6168
7154
|
const coopPath = coopDir(root);
|
|
6169
|
-
const graph =
|
|
6170
|
-
|
|
6171
|
-
|
|
7155
|
+
const graph = load_graph12(coopPath);
|
|
7156
|
+
const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery, sharedDefault(root, "delivery"));
|
|
7157
|
+
if (isVerboseRequested()) {
|
|
7158
|
+
for (const line of formatResolvedContextMessage({ delivery: resolvedDelivery })) {
|
|
7159
|
+
console.log(line);
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
const deliveryRef = resolvedDelivery.value;
|
|
7163
|
+
if (!deliveryRef) {
|
|
7164
|
+
throw new Error("delivery is not set; pass --delivery <name> or set a default working delivery with `coop use delivery <id>`.");
|
|
6172
7165
|
}
|
|
6173
|
-
const delivery = resolveDelivery(graph,
|
|
7166
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
6174
7167
|
const include = new Set(delivery.scope.include);
|
|
6175
7168
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
6176
7169
|
const scopedTasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -6210,7 +7203,7 @@ function runBurndown(options) {
|
|
|
6210
7203
|
}
|
|
6211
7204
|
function runCapacity(options) {
|
|
6212
7205
|
const root = resolveRepoRoot();
|
|
6213
|
-
const graph =
|
|
7206
|
+
const graph = load_graph12(coopDir(root));
|
|
6214
7207
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
6215
7208
|
if (deliveries.length === 0) {
|
|
6216
7209
|
console.log("No deliveries found.");
|
|
@@ -6298,11 +7291,11 @@ function registerWebhookCommand(program) {
|
|
|
6298
7291
|
}
|
|
6299
7292
|
|
|
6300
7293
|
// src/merge-driver/merge-driver.ts
|
|
6301
|
-
import
|
|
7294
|
+
import fs21 from "fs";
|
|
6302
7295
|
import os2 from "os";
|
|
6303
|
-
import
|
|
7296
|
+
import path25 from "path";
|
|
6304
7297
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6305
|
-
import { stringifyFrontmatter as
|
|
7298
|
+
import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
|
|
6306
7299
|
var STATUS_RANK = {
|
|
6307
7300
|
blocked: 0,
|
|
6308
7301
|
canceled: 0,
|
|
@@ -6383,33 +7376,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
|
|
|
6383
7376
|
return { ok: false, output: stdout };
|
|
6384
7377
|
}
|
|
6385
7378
|
function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
6386
|
-
const ancestorRaw =
|
|
6387
|
-
const oursRaw =
|
|
6388
|
-
const theirsRaw =
|
|
7379
|
+
const ancestorRaw = fs21.readFileSync(ancestorPath, "utf8");
|
|
7380
|
+
const oursRaw = fs21.readFileSync(oursPath, "utf8");
|
|
7381
|
+
const theirsRaw = fs21.readFileSync(theirsPath, "utf8");
|
|
6389
7382
|
const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
|
|
6390
7383
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
6391
7384
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
6392
7385
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
6393
|
-
const tempDir =
|
|
7386
|
+
const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
|
|
6394
7387
|
try {
|
|
6395
|
-
const ancestorBody =
|
|
6396
|
-
const oursBody =
|
|
6397
|
-
const theirsBody =
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
7388
|
+
const ancestorBody = path25.join(tempDir, "ancestor.md");
|
|
7389
|
+
const oursBody = path25.join(tempDir, "ours.md");
|
|
7390
|
+
const theirsBody = path25.join(tempDir, "theirs.md");
|
|
7391
|
+
fs21.writeFileSync(ancestorBody, ancestor.body, "utf8");
|
|
7392
|
+
fs21.writeFileSync(oursBody, ours.body, "utf8");
|
|
7393
|
+
fs21.writeFileSync(theirsBody, theirs.body, "utf8");
|
|
6401
7394
|
const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
|
|
6402
|
-
const output3 =
|
|
6403
|
-
|
|
7395
|
+
const output3 = stringifyFrontmatter6(mergedFrontmatter, mergedBody.output);
|
|
7396
|
+
fs21.writeFileSync(oursPath, output3, "utf8");
|
|
6404
7397
|
return mergedBody.ok ? 0 : 1;
|
|
6405
7398
|
} finally {
|
|
6406
|
-
|
|
7399
|
+
fs21.rmSync(tempDir, { recursive: true, force: true });
|
|
6407
7400
|
}
|
|
6408
7401
|
}
|
|
6409
7402
|
function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
6410
|
-
const ancestor = parseYamlContent3(
|
|
6411
|
-
const ours = parseYamlContent3(
|
|
6412
|
-
const theirs = parseYamlContent3(
|
|
7403
|
+
const ancestor = parseYamlContent3(fs21.readFileSync(ancestorPath, "utf8"), ancestorPath);
|
|
7404
|
+
const ours = parseYamlContent3(fs21.readFileSync(oursPath, "utf8"), oursPath);
|
|
7405
|
+
const theirs = parseYamlContent3(fs21.readFileSync(theirsPath, "utf8"), theirsPath);
|
|
6413
7406
|
const oursUpdated = asTimestamp(ours.updated);
|
|
6414
7407
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
6415
7408
|
const base = ancestor;
|
|
@@ -6419,7 +7412,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
|
6419
7412
|
const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
6420
7413
|
if (value !== void 0) merged[key] = value;
|
|
6421
7414
|
}
|
|
6422
|
-
|
|
7415
|
+
fs21.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
|
|
6423
7416
|
return 0;
|
|
6424
7417
|
}
|
|
6425
7418
|
function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
@@ -6429,12 +7422,17 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
6429
7422
|
return mergeTaskFile(ancestorPath, oursPath, theirsPath);
|
|
6430
7423
|
}
|
|
6431
7424
|
|
|
7425
|
+
// src/utils/not-implemented.ts
|
|
7426
|
+
function printNotImplemented(command, phase) {
|
|
7427
|
+
console.log(`${command}: Not yet implemented - coming in Phase ${phase}.`);
|
|
7428
|
+
}
|
|
7429
|
+
|
|
6432
7430
|
// src/index.ts
|
|
6433
7431
|
function readVersion() {
|
|
6434
7432
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
6435
|
-
const packageJsonPath =
|
|
7433
|
+
const packageJsonPath = path26.resolve(path26.dirname(currentFile), "..", "package.json");
|
|
6436
7434
|
try {
|
|
6437
|
-
const parsed = JSON.parse(
|
|
7435
|
+
const parsed = JSON.parse(fs22.readFileSync(packageJsonPath, "utf8"));
|
|
6438
7436
|
return parsed.version ?? "0.0.0";
|
|
6439
7437
|
} catch {
|
|
6440
7438
|
return "0.0.0";
|
|
@@ -6454,9 +7452,12 @@ function createProgram() {
|
|
|
6454
7452
|
program.option("-p, --project <id>", "Select the active COOP project");
|
|
6455
7453
|
registerInitCommand(program);
|
|
6456
7454
|
registerCreateCommand(program);
|
|
7455
|
+
registerCurrentCommand(program);
|
|
6457
7456
|
registerAssignCommand(program);
|
|
7457
|
+
registerCommentCommand(program);
|
|
6458
7458
|
registerAliasCommand(program);
|
|
6459
7459
|
registerConfigCommand(program);
|
|
7460
|
+
registerDepsCommand(program);
|
|
6460
7461
|
registerListCommand(program);
|
|
6461
7462
|
registerShowCommand(program);
|
|
6462
7463
|
registerTransitionCommand(program);
|
|
@@ -6465,16 +7466,22 @@ function createProgram() {
|
|
|
6465
7466
|
registerHelpAiCommand(program);
|
|
6466
7467
|
registerIndexCommand(program);
|
|
6467
7468
|
registerLogCommand(program);
|
|
7469
|
+
registerLogTimeCommand(program);
|
|
6468
7470
|
registerLifecycleCommands(program);
|
|
6469
7471
|
registerMigrateCommand(program);
|
|
6470
7472
|
registerNamingCommand(program);
|
|
6471
7473
|
registerPlanCommand(program);
|
|
7474
|
+
registerPromoteCommand(program);
|
|
6472
7475
|
registerProjectCommand(program);
|
|
7476
|
+
registerPromptCommand(program);
|
|
6473
7477
|
registerRefineCommand(program);
|
|
6474
7478
|
registerRunCommand(program);
|
|
7479
|
+
registerSearchCommand(program);
|
|
6475
7480
|
registerServeCommand(program);
|
|
6476
7481
|
registerStatusCommand(program);
|
|
6477
7482
|
registerUiCommand(program);
|
|
7483
|
+
registerUpdateCommand(program);
|
|
7484
|
+
registerUseCommand(program);
|
|
6478
7485
|
registerViewCommand(program);
|
|
6479
7486
|
registerWebhookCommand(program);
|
|
6480
7487
|
registerPhasePlaceholder(program, "ext", 3, "Plugin extension commands");
|
|
@@ -6534,7 +7541,7 @@ async function runCli(argv = process.argv) {
|
|
|
6534
7541
|
function isMainModule() {
|
|
6535
7542
|
const entry = process.argv[1];
|
|
6536
7543
|
if (!entry) return false;
|
|
6537
|
-
return
|
|
7544
|
+
return path26.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
6538
7545
|
}
|
|
6539
7546
|
if (isMainModule()) {
|
|
6540
7547
|
await runCli(process.argv);
|