@kitsy/coop 2.1.1 → 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/README.md +11 -0
- package/dist/index.js +1550 -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: [
|
|
@@ -2924,6 +3242,12 @@ var catalog = {
|
|
|
2924
3242
|
"coop reopen task <id>",
|
|
2925
3243
|
"coop transition task <id> <status>"
|
|
2926
3244
|
],
|
|
3245
|
+
lifecycle_requirements: [
|
|
3246
|
+
"`coop start task <id>` moves a ready task into `in_progress`.",
|
|
3247
|
+
"`coop review task <id>` requires the task to already be `in_progress` and moves it to `in_review`.",
|
|
3248
|
+
"`coop complete task <id>` requires the task to already be `in_review` and moves it to `done`.",
|
|
3249
|
+
"Do not call `coop complete task <id>` directly from `in_progress` unless COOP explicitly adds that transition later."
|
|
3250
|
+
],
|
|
2927
3251
|
unsupported_command_warnings: [
|
|
2928
3252
|
"Do not invent COOP commands such as `coop complete` when they are not present in `help-ai` output.",
|
|
2929
3253
|
"If a desired lifecycle action is not represented by an allowed command, state that explicitly instead of paraphrasing.",
|
|
@@ -2949,6 +3273,11 @@ var catalog = {
|
|
|
2949
3273
|
{ usage: "coop project list", purpose: "List projects in the current workspace." },
|
|
2950
3274
|
{ usage: "coop project show", purpose: "Show the active project id, name, path, and layout." },
|
|
2951
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." },
|
|
2952
3281
|
{ usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
|
|
2953
3282
|
{ usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
|
|
2954
3283
|
]
|
|
@@ -2987,8 +3316,10 @@ var catalog = {
|
|
|
2987
3316
|
commands: [
|
|
2988
3317
|
{ usage: "coop next task", purpose: "Show the top ready task using the default track or full workspace context." },
|
|
2989
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." },
|
|
2990
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." },
|
|
2991
|
-
{ 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." },
|
|
2992
3323
|
{ usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
|
|
2993
3324
|
{ usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
|
|
2994
3325
|
{ usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
|
|
@@ -3002,8 +3333,18 @@ var catalog = {
|
|
|
3002
3333
|
description: "Read backlog state, task details, and planning output.",
|
|
3003
3334
|
commands: [
|
|
3004
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." },
|
|
3005
3341
|
{ usage: "coop show task PM-101", purpose: "Show a task with acceptance, tests_required, refs, and runbook sections." },
|
|
3006
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." },
|
|
3007
3348
|
{ usage: "coop plan delivery MVP", purpose: "Run delivery feasibility analysis." },
|
|
3008
3349
|
{ usage: "coop plan delivery MVP --monte-carlo --iterations 5000", purpose: "Run probabilistic delivery forecasting." },
|
|
3009
3350
|
{ usage: "coop view velocity", purpose: "Show historical throughput." },
|
|
@@ -3014,7 +3355,13 @@ var catalog = {
|
|
|
3014
3355
|
name: "Execute And Integrate",
|
|
3015
3356
|
description: "Hand execution to an AI provider or code-agent CLI and expose COOP through MCP/API.",
|
|
3016
3357
|
commands: [
|
|
3358
|
+
{ usage: "coop help-ai --selection --format markdown", purpose: "Show the focused rules for deterministic task selection and workspace resolution." },
|
|
3359
|
+
{ usage: "coop help-ai --state-transitions --format json", purpose: "Show exact lifecycle commands, prerequisites, and warnings against invented transitions." },
|
|
3360
|
+
{ usage: "coop help-ai --artifacts --format markdown", purpose: "Show where contract-review, audit, and planning artifacts must be written." },
|
|
3361
|
+
{ usage: "coop help-ai --post-execution --format markdown", purpose: "Show what an agent must do after finishing the selected task." },
|
|
3362
|
+
{ usage: "coop help-ai --naming --format markdown", purpose: "Show naming guidance when IDs and naming templates matter." },
|
|
3017
3363
|
{ usage: "coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd", purpose: "Generate a strict agent bootstrap prompt for a repository and delivery." },
|
|
3364
|
+
{ usage: "coop help-ai --initial-prompt --rigour balanced --repo C:/path/to/repo --delivery MVP --command coop.cmd", purpose: "Generate the recommended balanced agent bootstrap prompt for day-to-day implementation work." },
|
|
3018
3365
|
{ usage: "coop config ai.provider codex_cli", purpose: "Use the installed Codex CLI as the execution/refinement backend." },
|
|
3019
3366
|
{ usage: "coop config ai.provider claude_cli", purpose: "Use the installed Claude CLI as the execution/refinement backend." },
|
|
3020
3367
|
{ usage: "coop config ai.provider gemini_cli", purpose: "Use the installed Gemini CLI as the execution/refinement backend." },
|
|
@@ -3040,12 +3387,124 @@ var catalog = {
|
|
|
3040
3387
|
execution_model: [
|
|
3041
3388
|
"Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
|
|
3042
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.",
|
|
3043
3391
|
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
|
|
3044
3392
|
"If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
|
|
3393
|
+
],
|
|
3394
|
+
post_execution_rules: [
|
|
3395
|
+
"After completing the currently selected task, stop and wait instead of automatically selecting the next task.",
|
|
3396
|
+
"Do not auto-start a second task in the same run unless the user explicitly asks you to continue.",
|
|
3397
|
+
"If you changed task state in COOP, report the exact command used and the resulting new state before doing anything else."
|
|
3045
3398
|
]
|
|
3046
3399
|
};
|
|
3400
|
+
function renderTopicPayload(topic) {
|
|
3401
|
+
if (topic === "state-transitions") {
|
|
3402
|
+
return {
|
|
3403
|
+
topic,
|
|
3404
|
+
allowed_lifecycle_commands: catalog.allowed_lifecycle_commands,
|
|
3405
|
+
lifecycle_requirements: catalog.lifecycle_requirements,
|
|
3406
|
+
warnings: catalog.unsupported_command_warnings
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
if (topic === "artifacts") {
|
|
3410
|
+
return {
|
|
3411
|
+
topic,
|
|
3412
|
+
artifact_policy: catalog.artifact_policy
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
if (topic === "post-execution") {
|
|
3416
|
+
return {
|
|
3417
|
+
topic,
|
|
3418
|
+
post_execution_rules: catalog.post_execution_rules
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
if (topic === "selection") {
|
|
3422
|
+
return {
|
|
3423
|
+
topic,
|
|
3424
|
+
selection_rules: catalog.selection_rules,
|
|
3425
|
+
workspace_rules: catalog.workspace_rules
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
return {
|
|
3429
|
+
topic,
|
|
3430
|
+
naming_guidance: [
|
|
3431
|
+
"Use `coop naming` to inspect the current naming template and token behavior.",
|
|
3432
|
+
'Use `coop naming preview "<title>"` before creating a new idea, task, or delivery if predictable IDs matter.',
|
|
3433
|
+
"Use `coop config id.naming ...` to override the default semantic naming template."
|
|
3434
|
+
]
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function renderAiHelpTopic(format, topic) {
|
|
3438
|
+
const payload = renderTopicPayload(topic);
|
|
3439
|
+
if (format === "json") {
|
|
3440
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
3441
|
+
`;
|
|
3442
|
+
}
|
|
3443
|
+
const lines = [];
|
|
3444
|
+
const bullet = (value) => `- ${value}`;
|
|
3445
|
+
const title = topic.split("-").map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
3446
|
+
lines.push(format === "markdown" ? `# COOP AI Help: ${title}` : `COOP AI Help: ${title}`, "");
|
|
3447
|
+
if (topic === "state-transitions") {
|
|
3448
|
+
lines.push(format === "markdown" ? "## Allowed Lifecycle Commands" : "Allowed Lifecycle Commands");
|
|
3449
|
+
for (const item of catalog.allowed_lifecycle_commands) {
|
|
3450
|
+
lines.push(bullet(`\`${item}\``));
|
|
3451
|
+
}
|
|
3452
|
+
lines.push("");
|
|
3453
|
+
lines.push(format === "markdown" ? "## Lifecycle Requirements" : "Lifecycle Requirements");
|
|
3454
|
+
for (const item of catalog.lifecycle_requirements) {
|
|
3455
|
+
lines.push(bullet(item));
|
|
3456
|
+
}
|
|
3457
|
+
lines.push("");
|
|
3458
|
+
lines.push(format === "markdown" ? "## Warnings" : "Warnings");
|
|
3459
|
+
for (const item of catalog.unsupported_command_warnings) {
|
|
3460
|
+
lines.push(bullet(item));
|
|
3461
|
+
}
|
|
3462
|
+
lines.push("");
|
|
3463
|
+
return `${lines.join("\n")}
|
|
3464
|
+
`;
|
|
3465
|
+
}
|
|
3466
|
+
if (topic === "artifacts") {
|
|
3467
|
+
lines.push(bullet(`Config key: \`${catalog.artifact_policy.config_key}\``));
|
|
3468
|
+
lines.push(bullet(`Default dir: \`${catalog.artifact_policy.default_dir}\``));
|
|
3469
|
+
lines.push(bullet(`Required for: ${catalog.artifact_policy.required_for.join(", ")}`));
|
|
3470
|
+
for (const item of catalog.artifact_policy.guidance) {
|
|
3471
|
+
lines.push(bullet(item));
|
|
3472
|
+
}
|
|
3473
|
+
lines.push("");
|
|
3474
|
+
return `${lines.join("\n")}
|
|
3475
|
+
`;
|
|
3476
|
+
}
|
|
3477
|
+
if (topic === "post-execution") {
|
|
3478
|
+
for (const item of catalog.post_execution_rules) {
|
|
3479
|
+
lines.push(bullet(item));
|
|
3480
|
+
}
|
|
3481
|
+
lines.push("");
|
|
3482
|
+
return `${lines.join("\n")}
|
|
3483
|
+
`;
|
|
3484
|
+
}
|
|
3485
|
+
if (topic === "selection") {
|
|
3486
|
+
lines.push(format === "markdown" ? "## Selection Rules" : "Selection Rules");
|
|
3487
|
+
for (const item of catalog.selection_rules) {
|
|
3488
|
+
lines.push(bullet(item));
|
|
3489
|
+
}
|
|
3490
|
+
lines.push("");
|
|
3491
|
+
lines.push(format === "markdown" ? "## Workspace Rules" : "Workspace Rules");
|
|
3492
|
+
for (const item of catalog.workspace_rules) {
|
|
3493
|
+
lines.push(bullet(item));
|
|
3494
|
+
}
|
|
3495
|
+
lines.push("");
|
|
3496
|
+
return `${lines.join("\n")}
|
|
3497
|
+
`;
|
|
3498
|
+
}
|
|
3499
|
+
for (const item of renderTopicPayload("naming").naming_guidance) {
|
|
3500
|
+
lines.push(bullet(item));
|
|
3501
|
+
}
|
|
3502
|
+
lines.push("");
|
|
3503
|
+
return `${lines.join("\n")}
|
|
3504
|
+
`;
|
|
3505
|
+
}
|
|
3047
3506
|
function normalizeRepoPath(repoPath) {
|
|
3048
|
-
return repoPath ?
|
|
3507
|
+
return repoPath ? path9.resolve(repoPath) : process.cwd();
|
|
3049
3508
|
}
|
|
3050
3509
|
function formatSelectionCommand(commandName, delivery, track) {
|
|
3051
3510
|
if (delivery) {
|
|
@@ -3082,22 +3541,33 @@ function renderInitialPrompt(options = {}) {
|
|
|
3082
3541
|
"",
|
|
3083
3542
|
"Rules:",
|
|
3084
3543
|
"- Learn COOP capabilities from `coop help-ai` before proposing commands.",
|
|
3544
|
+
`- If you are unsure how to pick work, run \`${commandName} help-ai --selection --format markdown\`.`,
|
|
3545
|
+
`- If you are unsure about lifecycle changes, run \`${commandName} help-ai --state-transitions --format markdown\`.`,
|
|
3546
|
+
`- If you are unsure where artifacts should go, run \`${commandName} help-ai --artifacts --format markdown\`.`,
|
|
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.`,
|
|
3085
3549
|
"- Use only commands that actually exist in COOP. Do not invent command names.",
|
|
3086
3550
|
"- Do not reprioritize work outside COOP unless the user explicitly overrides it.",
|
|
3087
3551
|
"- Select only the first ready task from the COOP readiness output.",
|
|
3088
|
-
"- Inspect the selected task with `coop show
|
|
3552
|
+
"- Inspect the selected task with `coop show <id>` before implementation.",
|
|
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.",
|
|
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.",
|
|
3089
3557
|
`- Write any contract-review, audit, or planning artifact under \`${artifactsDir}\` unless the user explicitly chooses another location.`
|
|
3090
3558
|
];
|
|
3091
3559
|
if (rigour === "strict") {
|
|
3092
3560
|
lines.push(
|
|
3093
3561
|
"- Before mentioning a COOP command, verify the exact command name from `coop help-ai --format json`.",
|
|
3094
3562
|
"- If a needed workflow is not supported by an exact COOP command, say that explicitly instead of inventing one.",
|
|
3095
|
-
"- Do not switch to another task unless COOP state changes or the user explicitly redirects you."
|
|
3563
|
+
"- Do not switch to another task unless COOP state changes or the user explicitly redirects you.",
|
|
3564
|
+
"- After executing or completing the selected task, stop and wait for user confirmation before picking another task."
|
|
3096
3565
|
);
|
|
3097
3566
|
} else if (rigour === "balanced") {
|
|
3098
3567
|
lines.push(
|
|
3099
3568
|
"- Prefer exact COOP commands from `coop help-ai --format json` when describing next actions.",
|
|
3100
|
-
"- If COOP lacks a named command for a desired action, state the limitation plainly."
|
|
3569
|
+
"- If COOP lacks a named command for a desired action, state the limitation plainly.",
|
|
3570
|
+
"- After finishing the selected task, report the resulting COOP state before proposing follow-up work."
|
|
3101
3571
|
);
|
|
3102
3572
|
} else {
|
|
3103
3573
|
lines.push("- Keep COOP as the authority for task selection and lifecycle state.");
|
|
@@ -3141,6 +3611,11 @@ function renderAiHelp(format) {
|
|
|
3141
3611
|
lines.push(bullet(`\`${item}\``));
|
|
3142
3612
|
}
|
|
3143
3613
|
lines.push("");
|
|
3614
|
+
lines.push(format === "markdown" ? "## Lifecycle Requirements" : "Lifecycle Requirements");
|
|
3615
|
+
for (const item of catalog.lifecycle_requirements) {
|
|
3616
|
+
lines.push(bullet(item));
|
|
3617
|
+
}
|
|
3618
|
+
lines.push("");
|
|
3144
3619
|
lines.push(format === "markdown" ? "## Unsupported / Invented Command Warnings" : "Unsupported / Invented Command Warnings");
|
|
3145
3620
|
for (const item of catalog.unsupported_command_warnings) {
|
|
3146
3621
|
lines.push(bullet(item));
|
|
@@ -3173,6 +3648,11 @@ function renderAiHelp(format) {
|
|
|
3173
3648
|
lines.push(bullet(item));
|
|
3174
3649
|
}
|
|
3175
3650
|
lines.push("");
|
|
3651
|
+
lines.push(format === "markdown" ? "## Post-Execution Rules" : "Post-Execution Rules");
|
|
3652
|
+
for (const item of catalog.post_execution_rules) {
|
|
3653
|
+
lines.push(bullet(item));
|
|
3654
|
+
}
|
|
3655
|
+
lines.push("");
|
|
3176
3656
|
return `${lines.join("\n")}
|
|
3177
3657
|
`;
|
|
3178
3658
|
}
|
|
@@ -3182,7 +3662,7 @@ function renderAiInitialPrompt(options = {}) {
|
|
|
3182
3662
|
|
|
3183
3663
|
// src/commands/help-ai.ts
|
|
3184
3664
|
function registerHelpAiCommand(program) {
|
|
3185
|
-
program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").option("--initial-prompt", "Print an agent bootstrap prompt instead of the capability catalog").option("--repo <path>", "Repository path to embed in the initial prompt").option("--delivery <delivery>", "Delivery to embed in the initial prompt").option("--track <track>", "Track to embed in the initial prompt").option("--command <name>", "Command name to embed, e.g. coop or coop.cmd", "coop").option("--rigour <level>", "Prompt rigour: strict | balanced | light", "balanced").option("--strict", "Shortcut for --rigour strict").option("--balanced", "Shortcut for --rigour balanced").option("--light", "Shortcut for --rigour light").action((options) => {
|
|
3665
|
+
program.command("help-ai").description("Print AI-oriented COOP capability and usage help").option("--format <format>", "Output format: text | json | markdown", "text").option("--initial-prompt", "Print an agent bootstrap prompt instead of the capability catalog").option("--topic <topic>", "Focused help topic: state-transitions | artifacts | post-execution | selection | naming").option("--state-transitions", "Focused help for lifecycle and state transition usage").option("--artifacts", "Focused help for artifact placement and policy").option("--post-execution", "Focused help for what to do after finishing the selected task").option("--selection", "Focused help for task selection and workspace resolution").option("--naming", "Focused help for naming IDs and naming templates").option("--repo <path>", "Repository path to embed in the initial prompt").option("--delivery <delivery>", "Delivery to embed in the initial prompt").option("--track <track>", "Track to embed in the initial prompt").option("--command <name>", "Command name to embed, e.g. coop or coop.cmd", "coop").option("--rigour <level>", "Prompt rigour: strict | balanced | light", "balanced").option("--strict", "Shortcut for --rigour strict").option("--balanced", "Shortcut for --rigour balanced").option("--light", "Shortcut for --rigour light").action((options) => {
|
|
3186
3666
|
const format = options.format ?? "text";
|
|
3187
3667
|
if (format !== "text" && format !== "json" && format !== "markdown") {
|
|
3188
3668
|
throw new Error(`Unsupported help-ai format '${format}'. Expected text|json|markdown.`);
|
|
@@ -3191,6 +3671,7 @@ function registerHelpAiCommand(program) {
|
|
|
3191
3671
|
if (rigour !== "strict" && rigour !== "balanced" && rigour !== "light") {
|
|
3192
3672
|
throw new Error(`Unsupported help-ai rigour '${rigour}'. Expected strict|balanced|light.`);
|
|
3193
3673
|
}
|
|
3674
|
+
const topic = resolveHelpTopic(options);
|
|
3194
3675
|
let artifactsDir = "docs";
|
|
3195
3676
|
const repoRoot = options.repo ? resolveRepoRoot(options.repo) : resolveRepoRoot();
|
|
3196
3677
|
try {
|
|
@@ -3206,13 +3687,43 @@ function registerHelpAiCommand(program) {
|
|
|
3206
3687
|
commandName: options.command,
|
|
3207
3688
|
rigour,
|
|
3208
3689
|
artifactsDir
|
|
3209
|
-
}) : renderAiHelp(format);
|
|
3690
|
+
}) : topic ? renderAiHelpTopic(format, topic) : renderAiHelp(format);
|
|
3210
3691
|
console.log(output3.trimEnd());
|
|
3211
3692
|
});
|
|
3212
3693
|
}
|
|
3694
|
+
function resolveHelpTopic(options) {
|
|
3695
|
+
const requestedTopics = [];
|
|
3696
|
+
if (options.topic) {
|
|
3697
|
+
requestedTopics.push(options.topic);
|
|
3698
|
+
}
|
|
3699
|
+
if (options.stateTransitions) {
|
|
3700
|
+
requestedTopics.push("state-transitions");
|
|
3701
|
+
}
|
|
3702
|
+
if (options.artifacts) {
|
|
3703
|
+
requestedTopics.push("artifacts");
|
|
3704
|
+
}
|
|
3705
|
+
if (options.postExecution) {
|
|
3706
|
+
requestedTopics.push("post-execution");
|
|
3707
|
+
}
|
|
3708
|
+
if (options.selection) {
|
|
3709
|
+
requestedTopics.push("selection");
|
|
3710
|
+
}
|
|
3711
|
+
if (options.naming) {
|
|
3712
|
+
requestedTopics.push("naming");
|
|
3713
|
+
}
|
|
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(", ")}.`);
|
|
3717
|
+
}
|
|
3718
|
+
const topic = unique3[0];
|
|
3719
|
+
if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
|
|
3720
|
+
throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
|
|
3721
|
+
}
|
|
3722
|
+
return topic;
|
|
3723
|
+
}
|
|
3213
3724
|
|
|
3214
3725
|
// src/commands/index.ts
|
|
3215
|
-
import
|
|
3726
|
+
import path10 from "path";
|
|
3216
3727
|
import { IndexManager as IndexManager2 } from "@kitsy/coop-core";
|
|
3217
3728
|
function runStatus(options) {
|
|
3218
3729
|
const root = resolveRepoRoot();
|
|
@@ -3222,7 +3733,7 @@ function runStatus(options) {
|
|
|
3222
3733
|
const freshness = status.stale ? "stale" : "fresh";
|
|
3223
3734
|
const existsText = status.exists ? "present" : "missing";
|
|
3224
3735
|
console.log(`[COOP] index ${existsText}, ${freshness}`);
|
|
3225
|
-
console.log(`[COOP] graph: ${
|
|
3736
|
+
console.log(`[COOP] graph: ${path10.relative(root, status.graph_path)}`);
|
|
3226
3737
|
if (status.generated_at) {
|
|
3227
3738
|
console.log(`[COOP] generated_at: ${status.generated_at}`);
|
|
3228
3739
|
}
|
|
@@ -3245,7 +3756,7 @@ function runRebuild() {
|
|
|
3245
3756
|
const graph = manager.build_full_index();
|
|
3246
3757
|
const elapsed = Date.now() - start;
|
|
3247
3758
|
console.log(`[COOP] index rebuilt: ${graph.nodes.size} tasks (${elapsed} ms)`);
|
|
3248
|
-
console.log(`[COOP] graph: ${
|
|
3759
|
+
console.log(`[COOP] graph: ${path10.relative(root, manager.graphPath)}`);
|
|
3249
3760
|
}
|
|
3250
3761
|
function registerIndexCommand(program) {
|
|
3251
3762
|
const index = program.command("index").description("Index management commands");
|
|
@@ -3261,18 +3772,18 @@ function registerIndexCommand(program) {
|
|
|
3261
3772
|
}
|
|
3262
3773
|
|
|
3263
3774
|
// src/commands/init.ts
|
|
3264
|
-
import
|
|
3265
|
-
import
|
|
3775
|
+
import fs10 from "fs";
|
|
3776
|
+
import path13 from "path";
|
|
3266
3777
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3267
3778
|
import { createInterface } from "readline/promises";
|
|
3268
3779
|
import { stdin as input, stdout as output } from "process";
|
|
3269
3780
|
import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
3270
3781
|
|
|
3271
3782
|
// src/hooks/pre-commit.ts
|
|
3272
|
-
import
|
|
3273
|
-
import
|
|
3783
|
+
import fs8 from "fs";
|
|
3784
|
+
import path11 from "path";
|
|
3274
3785
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3275
|
-
import { detect_cycle, parseTaskContent, parseTaskFile as
|
|
3786
|
+
import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural6 } from "@kitsy/coop-core";
|
|
3276
3787
|
var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
|
|
3277
3788
|
var HOOK_BLOCK_END = "# COOP_PRE_COMMIT_END";
|
|
3278
3789
|
function runGit(repoRoot, args, allowFailure = false) {
|
|
@@ -3301,28 +3812,28 @@ function projectRootFromRelativePath(repoRoot, relativePath) {
|
|
|
3301
3812
|
const normalized = toPosixPath2(relativePath);
|
|
3302
3813
|
const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
|
|
3303
3814
|
if (projectMatch?.[1]) {
|
|
3304
|
-
return
|
|
3815
|
+
return path11.join(repoRoot, ".coop", "projects", projectMatch[1]);
|
|
3305
3816
|
}
|
|
3306
3817
|
if (normalized.startsWith(".coop/tasks/")) {
|
|
3307
|
-
return
|
|
3818
|
+
return path11.join(repoRoot, ".coop");
|
|
3308
3819
|
}
|
|
3309
3820
|
throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
|
|
3310
3821
|
}
|
|
3311
3822
|
function listTaskFilesForProject(projectRoot) {
|
|
3312
|
-
const tasksDir =
|
|
3313
|
-
if (!
|
|
3823
|
+
const tasksDir = path11.join(projectRoot, "tasks");
|
|
3824
|
+
if (!fs8.existsSync(tasksDir)) return [];
|
|
3314
3825
|
const out = [];
|
|
3315
3826
|
const stack = [tasksDir];
|
|
3316
3827
|
while (stack.length > 0) {
|
|
3317
3828
|
const current = stack.pop();
|
|
3318
|
-
const entries =
|
|
3829
|
+
const entries = fs8.readdirSync(current, { withFileTypes: true });
|
|
3319
3830
|
for (const entry of entries) {
|
|
3320
|
-
const fullPath =
|
|
3831
|
+
const fullPath = path11.join(current, entry.name);
|
|
3321
3832
|
if (entry.isDirectory()) {
|
|
3322
3833
|
stack.push(fullPath);
|
|
3323
3834
|
continue;
|
|
3324
3835
|
}
|
|
3325
|
-
if (entry.isFile() &&
|
|
3836
|
+
if (entry.isFile() && path11.extname(entry.name).toLowerCase() === ".md") {
|
|
3326
3837
|
out.push(fullPath);
|
|
3327
3838
|
}
|
|
3328
3839
|
}
|
|
@@ -3341,7 +3852,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3341
3852
|
const errors = [];
|
|
3342
3853
|
const staged = [];
|
|
3343
3854
|
for (const relativePath of relativePaths) {
|
|
3344
|
-
const absolutePath =
|
|
3855
|
+
const absolutePath = path11.join(repoRoot, ...relativePath.split("/"));
|
|
3345
3856
|
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
3346
3857
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
3347
3858
|
if (!stagedBlob) {
|
|
@@ -3356,7 +3867,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
3356
3867
|
errors.push(`[COOP] ${message}`);
|
|
3357
3868
|
continue;
|
|
3358
3869
|
}
|
|
3359
|
-
const issues =
|
|
3870
|
+
const issues = validateStructural6(task, { filePath: absolutePath });
|
|
3360
3871
|
for (const issue of issues) {
|
|
3361
3872
|
errors.push(`[COOP] ${relativePath}: ${issue.message}`);
|
|
3362
3873
|
}
|
|
@@ -3405,13 +3916,13 @@ function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
|
3405
3916
|
}
|
|
3406
3917
|
const tasks = [];
|
|
3407
3918
|
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
3408
|
-
const normalized = toPosixPath2(
|
|
3919
|
+
const normalized = toPosixPath2(path11.resolve(filePath));
|
|
3409
3920
|
const stagedTask = stagedByPath.get(normalized);
|
|
3410
3921
|
if (stagedTask) {
|
|
3411
3922
|
tasks.push(stagedTask);
|
|
3412
3923
|
continue;
|
|
3413
3924
|
}
|
|
3414
|
-
tasks.push(
|
|
3925
|
+
tasks.push(parseTaskFile8(filePath).task);
|
|
3415
3926
|
}
|
|
3416
3927
|
return tasks;
|
|
3417
3928
|
}
|
|
@@ -3435,7 +3946,7 @@ function runPreCommitChecks(repoRoot) {
|
|
|
3435
3946
|
const graph = buildGraphForCycleCheck(tasks);
|
|
3436
3947
|
const cycle = detect_cycle(graph);
|
|
3437
3948
|
if (cycle) {
|
|
3438
|
-
const projectLabel = toPosixPath2(
|
|
3949
|
+
const projectLabel = toPosixPath2(path11.relative(repoRoot, projectRoot));
|
|
3439
3950
|
errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
|
|
3440
3951
|
}
|
|
3441
3952
|
} catch (error) {
|
|
@@ -3467,9 +3978,9 @@ function hookScriptBlock() {
|
|
|
3467
3978
|
].join("\n");
|
|
3468
3979
|
}
|
|
3469
3980
|
function installPreCommitHook(repoRoot) {
|
|
3470
|
-
const hookPath =
|
|
3471
|
-
const hookDir =
|
|
3472
|
-
if (!
|
|
3981
|
+
const hookPath = path11.join(repoRoot, ".git", "hooks", "pre-commit");
|
|
3982
|
+
const hookDir = path11.dirname(hookPath);
|
|
3983
|
+
if (!fs8.existsSync(hookDir)) {
|
|
3473
3984
|
return {
|
|
3474
3985
|
installed: false,
|
|
3475
3986
|
hookPath,
|
|
@@ -3477,18 +3988,18 @@ function installPreCommitHook(repoRoot) {
|
|
|
3477
3988
|
};
|
|
3478
3989
|
}
|
|
3479
3990
|
const block = hookScriptBlock();
|
|
3480
|
-
if (!
|
|
3991
|
+
if (!fs8.existsSync(hookPath)) {
|
|
3481
3992
|
const content = ["#!/bin/sh", "", block].join("\n");
|
|
3482
|
-
|
|
3993
|
+
fs8.writeFileSync(hookPath, content, "utf8");
|
|
3483
3994
|
} else {
|
|
3484
|
-
const existing =
|
|
3995
|
+
const existing = fs8.readFileSync(hookPath, "utf8");
|
|
3485
3996
|
if (!existing.includes(HOOK_BLOCK_START)) {
|
|
3486
3997
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3487
|
-
|
|
3998
|
+
fs8.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3488
3999
|
}
|
|
3489
4000
|
}
|
|
3490
4001
|
try {
|
|
3491
|
-
|
|
4002
|
+
fs8.chmodSync(hookPath, 493);
|
|
3492
4003
|
} catch {
|
|
3493
4004
|
}
|
|
3494
4005
|
return {
|
|
@@ -3499,15 +4010,15 @@ function installPreCommitHook(repoRoot) {
|
|
|
3499
4010
|
}
|
|
3500
4011
|
|
|
3501
4012
|
// src/hooks/post-merge-validate.ts
|
|
3502
|
-
import
|
|
3503
|
-
import
|
|
4013
|
+
import fs9 from "fs";
|
|
4014
|
+
import path12 from "path";
|
|
3504
4015
|
import { list_projects } from "@kitsy/coop-core";
|
|
3505
|
-
import { load_graph as
|
|
4016
|
+
import { load_graph as load_graph5, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
3506
4017
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
3507
4018
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
3508
4019
|
function runPostMergeValidate(repoRoot) {
|
|
3509
|
-
const workspaceDir =
|
|
3510
|
-
if (!
|
|
4020
|
+
const workspaceDir = path12.join(repoRoot, ".coop");
|
|
4021
|
+
if (!fs9.existsSync(workspaceDir)) {
|
|
3511
4022
|
return {
|
|
3512
4023
|
ok: true,
|
|
3513
4024
|
warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
|
|
@@ -3523,7 +4034,7 @@ function runPostMergeValidate(repoRoot) {
|
|
|
3523
4034
|
}
|
|
3524
4035
|
const warnings = [];
|
|
3525
4036
|
for (const project of projects) {
|
|
3526
|
-
const graph =
|
|
4037
|
+
const graph = load_graph5(project.root);
|
|
3527
4038
|
const issues = validate_graph2(graph);
|
|
3528
4039
|
for (const issue of issues) {
|
|
3529
4040
|
warnings.push(`[COOP] post-merge warning [${project.id}] [${issue.invariant}] ${issue.message}`);
|
|
@@ -3559,9 +4070,9 @@ function postMergeHookBlock() {
|
|
|
3559
4070
|
].join("\n");
|
|
3560
4071
|
}
|
|
3561
4072
|
function installPostMergeHook(repoRoot) {
|
|
3562
|
-
const hookPath =
|
|
3563
|
-
const hookDir =
|
|
3564
|
-
if (!
|
|
4073
|
+
const hookPath = path12.join(repoRoot, ".git", "hooks", "post-merge");
|
|
4074
|
+
const hookDir = path12.dirname(hookPath);
|
|
4075
|
+
if (!fs9.existsSync(hookDir)) {
|
|
3565
4076
|
return {
|
|
3566
4077
|
installed: false,
|
|
3567
4078
|
hookPath,
|
|
@@ -3569,17 +4080,17 @@ function installPostMergeHook(repoRoot) {
|
|
|
3569
4080
|
};
|
|
3570
4081
|
}
|
|
3571
4082
|
const block = postMergeHookBlock();
|
|
3572
|
-
if (!
|
|
3573
|
-
|
|
4083
|
+
if (!fs9.existsSync(hookPath)) {
|
|
4084
|
+
fs9.writeFileSync(hookPath, ["#!/bin/sh", "", block].join("\n"), "utf8");
|
|
3574
4085
|
} else {
|
|
3575
|
-
const existing =
|
|
4086
|
+
const existing = fs9.readFileSync(hookPath, "utf8");
|
|
3576
4087
|
if (!existing.includes(HOOK_BLOCK_START2)) {
|
|
3577
4088
|
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
3578
|
-
|
|
4089
|
+
fs9.writeFileSync(hookPath, `${existing}${suffix}${block}`, "utf8");
|
|
3579
4090
|
}
|
|
3580
4091
|
}
|
|
3581
4092
|
try {
|
|
3582
|
-
|
|
4093
|
+
fs9.chmodSync(hookPath, 493);
|
|
3583
4094
|
} catch {
|
|
3584
4095
|
}
|
|
3585
4096
|
return {
|
|
@@ -3745,40 +4256,40 @@ tmp/
|
|
|
3745
4256
|
*.tmp
|
|
3746
4257
|
`;
|
|
3747
4258
|
function ensureDir(dirPath) {
|
|
3748
|
-
|
|
4259
|
+
fs10.mkdirSync(dirPath, { recursive: true });
|
|
3749
4260
|
}
|
|
3750
4261
|
function writeIfMissing(filePath, content) {
|
|
3751
|
-
if (!
|
|
3752
|
-
|
|
4262
|
+
if (!fs10.existsSync(filePath)) {
|
|
4263
|
+
fs10.writeFileSync(filePath, content, "utf8");
|
|
3753
4264
|
}
|
|
3754
4265
|
}
|
|
3755
4266
|
function ensureGitignoreEntry(root, entry) {
|
|
3756
|
-
const gitignorePath =
|
|
3757
|
-
if (!
|
|
3758
|
-
|
|
4267
|
+
const gitignorePath = path13.join(root, ".gitignore");
|
|
4268
|
+
if (!fs10.existsSync(gitignorePath)) {
|
|
4269
|
+
fs10.writeFileSync(gitignorePath, `${entry}
|
|
3759
4270
|
`, "utf8");
|
|
3760
4271
|
return;
|
|
3761
4272
|
}
|
|
3762
|
-
const content =
|
|
4273
|
+
const content = fs10.readFileSync(gitignorePath, "utf8");
|
|
3763
4274
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3764
4275
|
if (!lines.includes(entry)) {
|
|
3765
4276
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3766
|
-
|
|
4277
|
+
fs10.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
3767
4278
|
`, "utf8");
|
|
3768
4279
|
}
|
|
3769
4280
|
}
|
|
3770
4281
|
function ensureGitattributesEntry(root, entry) {
|
|
3771
|
-
const attrsPath =
|
|
3772
|
-
if (!
|
|
3773
|
-
|
|
4282
|
+
const attrsPath = path13.join(root, ".gitattributes");
|
|
4283
|
+
if (!fs10.existsSync(attrsPath)) {
|
|
4284
|
+
fs10.writeFileSync(attrsPath, `${entry}
|
|
3774
4285
|
`, "utf8");
|
|
3775
4286
|
return;
|
|
3776
4287
|
}
|
|
3777
|
-
const content =
|
|
4288
|
+
const content = fs10.readFileSync(attrsPath, "utf8");
|
|
3778
4289
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3779
4290
|
if (!lines.includes(entry)) {
|
|
3780
4291
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
3781
|
-
|
|
4292
|
+
fs10.writeFileSync(attrsPath, `${content}${suffix}${entry}
|
|
3782
4293
|
`, "utf8");
|
|
3783
4294
|
}
|
|
3784
4295
|
}
|
|
@@ -3864,7 +4375,7 @@ function registerInitCommand(program) {
|
|
|
3864
4375
|
const workspaceDir = coopWorkspaceDir(root);
|
|
3865
4376
|
const identity = await resolveInitIdentity(root, options);
|
|
3866
4377
|
const projectId = identity.projectId;
|
|
3867
|
-
const projectRoot =
|
|
4378
|
+
const projectRoot = path13.join(workspaceDir, "projects", projectId);
|
|
3868
4379
|
const dirs = [
|
|
3869
4380
|
"ideas",
|
|
3870
4381
|
"tasks",
|
|
@@ -3880,23 +4391,23 @@ function registerInitCommand(program) {
|
|
|
3880
4391
|
"history/deliveries",
|
|
3881
4392
|
".index"
|
|
3882
4393
|
];
|
|
3883
|
-
ensureDir(
|
|
4394
|
+
ensureDir(path13.join(workspaceDir, "projects"));
|
|
3884
4395
|
for (const dir of dirs) {
|
|
3885
|
-
ensureDir(
|
|
4396
|
+
ensureDir(path13.join(projectRoot, dir));
|
|
3886
4397
|
}
|
|
3887
4398
|
writeIfMissing(
|
|
3888
|
-
|
|
4399
|
+
path13.join(projectRoot, "config.yml"),
|
|
3889
4400
|
buildProjectConfig(projectId, identity.projectName, identity.projectAliases, identity.namingTemplate)
|
|
3890
4401
|
);
|
|
3891
|
-
if (!
|
|
4402
|
+
if (!fs10.existsSync(path13.join(projectRoot, "schema-version"))) {
|
|
3892
4403
|
write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
|
|
3893
4404
|
}
|
|
3894
|
-
writeIfMissing(
|
|
3895
|
-
writeIfMissing(
|
|
3896
|
-
writeIfMissing(
|
|
3897
|
-
writeIfMissing(
|
|
3898
|
-
writeIfMissing(
|
|
3899
|
-
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);
|
|
3900
4411
|
writeWorkspaceConfig(root, { version: 2, current_project: projectId });
|
|
3901
4412
|
ensureGitignoreEntry(root, ".coop/logs/");
|
|
3902
4413
|
ensureGitignoreEntry(root, ".coop/tmp/");
|
|
@@ -3906,18 +4417,18 @@ function registerInitCommand(program) {
|
|
|
3906
4417
|
const project = resolveProject(root, projectId);
|
|
3907
4418
|
console.log("Initialized COOP workspace.");
|
|
3908
4419
|
console.log(`- Root: ${root}`);
|
|
3909
|
-
console.log(`- Workspace: ${
|
|
3910
|
-
console.log(`- Project: ${project.id} (${
|
|
4420
|
+
console.log(`- Workspace: ${path13.relative(root, workspaceDir)}`);
|
|
4421
|
+
console.log(`- Project: ${project.id} (${path13.relative(root, project.root)})`);
|
|
3911
4422
|
console.log(`- Name: ${identity.projectName}`);
|
|
3912
4423
|
console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
3913
4424
|
console.log(`- ID naming: ${identity.namingTemplate}`);
|
|
3914
4425
|
console.log(`- ${preCommitHook.message}`);
|
|
3915
4426
|
if (preCommitHook.installed) {
|
|
3916
|
-
console.log(`- Hook: ${
|
|
4427
|
+
console.log(`- Hook: ${path13.relative(root, preCommitHook.hookPath)}`);
|
|
3917
4428
|
}
|
|
3918
4429
|
console.log(`- ${postMergeHook.message}`);
|
|
3919
4430
|
if (postMergeHook.installed) {
|
|
3920
|
-
console.log(`- Hook: ${
|
|
4431
|
+
console.log(`- Hook: ${path13.relative(root, postMergeHook.hookPath)}`);
|
|
3921
4432
|
}
|
|
3922
4433
|
console.log(`- ${mergeDrivers}`);
|
|
3923
4434
|
console.log("- Next steps:");
|
|
@@ -3928,8 +4439,8 @@ function registerInitCommand(program) {
|
|
|
3928
4439
|
}
|
|
3929
4440
|
|
|
3930
4441
|
// src/commands/lifecycle.ts
|
|
3931
|
-
import
|
|
3932
|
-
import { parseTaskFile as
|
|
4442
|
+
import path14 from "path";
|
|
4443
|
+
import { parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
3933
4444
|
var lifecycleVerbs = [
|
|
3934
4445
|
{
|
|
3935
4446
|
name: "review",
|
|
@@ -3964,8 +4475,8 @@ var lifecycleVerbs = [
|
|
|
3964
4475
|
];
|
|
3965
4476
|
function currentTaskSelection(root, id) {
|
|
3966
4477
|
const reference = resolveReference(root, id, "task");
|
|
3967
|
-
const filePath =
|
|
3968
|
-
const parsed =
|
|
4478
|
+
const filePath = path14.join(root, ...reference.file.split("/"));
|
|
4479
|
+
const parsed = parseTaskFile9(filePath);
|
|
3969
4480
|
return formatSelectedTask(
|
|
3970
4481
|
{
|
|
3971
4482
|
task: parsed.task,
|
|
@@ -3989,16 +4500,9 @@ function registerLifecycleCommands(program) {
|
|
|
3989
4500
|
}
|
|
3990
4501
|
|
|
3991
4502
|
// src/commands/list.ts
|
|
3992
|
-
import
|
|
3993
|
-
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";
|
|
3994
4505
|
import chalk2 from "chalk";
|
|
3995
|
-
|
|
3996
|
-
// src/utils/not-implemented.ts
|
|
3997
|
-
function printNotImplemented(command, phase) {
|
|
3998
|
-
console.log(`${command}: Not yet implemented - coming in Phase ${phase}.`);
|
|
3999
|
-
}
|
|
4000
|
-
|
|
4001
|
-
// src/commands/list.ts
|
|
4002
4506
|
function statusColor(status) {
|
|
4003
4507
|
switch (status) {
|
|
4004
4508
|
case "done":
|
|
@@ -4018,49 +4522,81 @@ function statusColor(status) {
|
|
|
4018
4522
|
function sortByIdAsc(items) {
|
|
4019
4523
|
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
4020
4524
|
}
|
|
4021
|
-
function
|
|
4525
|
+
function loadTasks2(root) {
|
|
4022
4526
|
return listTaskFiles(root).map((filePath) => ({
|
|
4023
|
-
task:
|
|
4527
|
+
task: parseTaskFile10(filePath).task,
|
|
4024
4528
|
filePath
|
|
4025
4529
|
}));
|
|
4026
4530
|
}
|
|
4027
4531
|
function loadIdeas(root) {
|
|
4028
4532
|
return listIdeaFiles(root).map((filePath) => ({
|
|
4029
|
-
idea:
|
|
4533
|
+
idea: parseIdeaFile4(filePath).idea,
|
|
4030
4534
|
filePath
|
|
4031
4535
|
}));
|
|
4032
4536
|
}
|
|
4033
4537
|
function listTasks(options) {
|
|
4034
4538
|
const root = resolveRepoRoot();
|
|
4035
4539
|
ensureCoopInitialized(root);
|
|
4036
|
-
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;
|
|
4037
4562
|
if (options.status && task.status !== options.status) return false;
|
|
4038
|
-
if (
|
|
4039
|
-
|
|
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
|
+
}
|
|
4040
4572
|
return true;
|
|
4041
4573
|
}).map(({ task, filePath }) => ({
|
|
4042
4574
|
id: task.id,
|
|
4043
4575
|
title: task.title,
|
|
4044
4576
|
status: task.status,
|
|
4045
|
-
priority: task.
|
|
4577
|
+
priority: taskEffectivePriority(task, resolvedTrack.value),
|
|
4046
4578
|
track: task.track ?? "-",
|
|
4579
|
+
assignee: task.assignee ?? "-",
|
|
4580
|
+
delivery: task.delivery ?? "-",
|
|
4047
4581
|
filePath
|
|
4048
4582
|
}));
|
|
4049
|
-
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);
|
|
4050
4584
|
if (sorted.length === 0) {
|
|
4051
4585
|
console.log("No tasks found.");
|
|
4052
4586
|
return;
|
|
4053
4587
|
}
|
|
4054
4588
|
console.log(
|
|
4055
4589
|
formatTable(
|
|
4056
|
-
["ID", "Title", "Status", "Priority", "Track", "File"],
|
|
4590
|
+
["ID", "Title", "Status", "Priority", "Track", "Assignee", "Delivery", "File"],
|
|
4057
4591
|
sorted.map((entry) => [
|
|
4058
4592
|
entry.id,
|
|
4059
4593
|
entry.title,
|
|
4060
4594
|
statusColor(entry.status),
|
|
4061
4595
|
entry.priority,
|
|
4062
4596
|
entry.track,
|
|
4063
|
-
|
|
4597
|
+
entry.assignee,
|
|
4598
|
+
entry.delivery,
|
|
4599
|
+
path15.relative(root, entry.filePath)
|
|
4064
4600
|
])
|
|
4065
4601
|
)
|
|
4066
4602
|
);
|
|
@@ -4095,16 +4631,31 @@ function listIdeas(options) {
|
|
|
4095
4631
|
statusColor(entry.status),
|
|
4096
4632
|
entry.priority,
|
|
4097
4633
|
entry.track,
|
|
4098
|
-
|
|
4634
|
+
path15.relative(root, entry.filePath)
|
|
4099
4635
|
])
|
|
4100
4636
|
)
|
|
4101
4637
|
);
|
|
4102
4638
|
console.log(`
|
|
4103
4639
|
Total ideas: ${sorted.length}`);
|
|
4104
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
|
+
}
|
|
4105
4656
|
function registerListCommand(program) {
|
|
4106
4657
|
const list = program.command("list").description("List COOP entities");
|
|
4107
|
-
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) => {
|
|
4108
4659
|
listTasks(options);
|
|
4109
4660
|
});
|
|
4110
4661
|
list.command("ideas").description("List ideas").option("--status <status>", "Filter by status").action((options) => {
|
|
@@ -4113,32 +4664,32 @@ function registerListCommand(program) {
|
|
|
4113
4664
|
list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
|
|
4114
4665
|
listAliasRows(pattern);
|
|
4115
4666
|
});
|
|
4116
|
-
list.command("deliveries").description("List deliveries
|
|
4117
|
-
|
|
4667
|
+
list.command("deliveries").description("List deliveries").action(() => {
|
|
4668
|
+
listDeliveries();
|
|
4118
4669
|
});
|
|
4119
4670
|
}
|
|
4120
4671
|
|
|
4121
4672
|
// src/utils/logger.ts
|
|
4122
|
-
import
|
|
4123
|
-
import
|
|
4673
|
+
import fs11 from "fs";
|
|
4674
|
+
import path16 from "path";
|
|
4124
4675
|
function resolveRepoSafe(start = process.cwd()) {
|
|
4125
4676
|
try {
|
|
4126
4677
|
return resolveRepoRoot(start);
|
|
4127
4678
|
} catch {
|
|
4128
|
-
return
|
|
4679
|
+
return path16.resolve(start);
|
|
4129
4680
|
}
|
|
4130
4681
|
}
|
|
4131
4682
|
function resolveCliLogFile(start = process.cwd()) {
|
|
4132
4683
|
const root = resolveRepoSafe(start);
|
|
4133
4684
|
const workspace = coopWorkspaceDir(root);
|
|
4134
|
-
if (
|
|
4135
|
-
return
|
|
4685
|
+
if (fs11.existsSync(workspace)) {
|
|
4686
|
+
return path16.join(workspace, "logs", "cli.log");
|
|
4136
4687
|
}
|
|
4137
|
-
return
|
|
4688
|
+
return path16.join(resolveCoopHome(), "logs", "cli.log");
|
|
4138
4689
|
}
|
|
4139
4690
|
function appendLogEntry(entry, logFile) {
|
|
4140
|
-
|
|
4141
|
-
|
|
4691
|
+
fs11.mkdirSync(path16.dirname(logFile), { recursive: true });
|
|
4692
|
+
fs11.appendFileSync(logFile, `${JSON.stringify(entry)}
|
|
4142
4693
|
`, "utf8");
|
|
4143
4694
|
}
|
|
4144
4695
|
function logCliError(error, start = process.cwd()) {
|
|
@@ -4177,8 +4728,8 @@ function parseLogLine(line) {
|
|
|
4177
4728
|
}
|
|
4178
4729
|
function readLastCliLog(start = process.cwd()) {
|
|
4179
4730
|
const logFile = resolveCliLogFile(start);
|
|
4180
|
-
if (!
|
|
4181
|
-
const content =
|
|
4731
|
+
if (!fs11.existsSync(logFile)) return null;
|
|
4732
|
+
const content = fs11.readFileSync(logFile, "utf8");
|
|
4182
4733
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4183
4734
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
4184
4735
|
const entry = parseLogLine(lines[i] ?? "");
|
|
@@ -4212,9 +4763,37 @@ function registerLogCommand(program) {
|
|
|
4212
4763
|
});
|
|
4213
4764
|
}
|
|
4214
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
|
+
|
|
4215
4794
|
// src/commands/migrate.ts
|
|
4216
|
-
import
|
|
4217
|
-
import
|
|
4795
|
+
import fs12 from "fs";
|
|
4796
|
+
import path17 from "path";
|
|
4218
4797
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4219
4798
|
import { stdin as input2, stdout as output2 } from "process";
|
|
4220
4799
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
|
|
@@ -4233,22 +4812,22 @@ function parseTargetVersion(raw) {
|
|
|
4233
4812
|
return parsed;
|
|
4234
4813
|
}
|
|
4235
4814
|
function writeIfMissing2(filePath, content) {
|
|
4236
|
-
if (!
|
|
4237
|
-
|
|
4815
|
+
if (!fs12.existsSync(filePath)) {
|
|
4816
|
+
fs12.writeFileSync(filePath, content, "utf8");
|
|
4238
4817
|
}
|
|
4239
4818
|
}
|
|
4240
4819
|
function ensureGitignoreEntry2(root, entry) {
|
|
4241
|
-
const gitignorePath =
|
|
4242
|
-
if (!
|
|
4243
|
-
|
|
4820
|
+
const gitignorePath = path17.join(root, ".gitignore");
|
|
4821
|
+
if (!fs12.existsSync(gitignorePath)) {
|
|
4822
|
+
fs12.writeFileSync(gitignorePath, `${entry}
|
|
4244
4823
|
`, "utf8");
|
|
4245
4824
|
return;
|
|
4246
4825
|
}
|
|
4247
|
-
const content =
|
|
4826
|
+
const content = fs12.readFileSync(gitignorePath, "utf8");
|
|
4248
4827
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
4249
4828
|
if (!lines.includes(entry)) {
|
|
4250
4829
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
4251
|
-
|
|
4830
|
+
fs12.writeFileSync(gitignorePath, `${content}${suffix}${entry}
|
|
4252
4831
|
`, "utf8");
|
|
4253
4832
|
}
|
|
4254
4833
|
}
|
|
@@ -4273,7 +4852,7 @@ function legacyWorkspaceProjectEntries(root) {
|
|
|
4273
4852
|
"backlog",
|
|
4274
4853
|
"plans",
|
|
4275
4854
|
"releases"
|
|
4276
|
-
].filter((entry) =>
|
|
4855
|
+
].filter((entry) => fs12.existsSync(path17.join(workspaceDir, entry)));
|
|
4277
4856
|
}
|
|
4278
4857
|
function normalizeProjectId2(value) {
|
|
4279
4858
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
@@ -4318,7 +4897,7 @@ async function promptProjectIdentity(defaults, options) {
|
|
|
4318
4897
|
async function resolveMigrationIdentity(root, options) {
|
|
4319
4898
|
const existing = readCoopConfig(root);
|
|
4320
4899
|
const defaults = {
|
|
4321
|
-
projectName: existing.projectName ||
|
|
4900
|
+
projectName: existing.projectName || path17.basename(root),
|
|
4322
4901
|
projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
|
|
4323
4902
|
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
|
|
4324
4903
|
};
|
|
@@ -4338,22 +4917,22 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4338
4917
|
throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
|
|
4339
4918
|
}
|
|
4340
4919
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4341
|
-
if (!
|
|
4920
|
+
if (!fs12.existsSync(workspaceDir)) {
|
|
4342
4921
|
throw new Error("Missing .coop directory. Run 'coop init' first.");
|
|
4343
4922
|
}
|
|
4344
|
-
const projectsDir =
|
|
4923
|
+
const projectsDir = path17.join(workspaceDir, "projects");
|
|
4345
4924
|
const legacyEntries = legacyWorkspaceProjectEntries(root);
|
|
4346
|
-
if (legacyEntries.length === 0 &&
|
|
4925
|
+
if (legacyEntries.length === 0 && fs12.existsSync(projectsDir)) {
|
|
4347
4926
|
console.log("[COOP] workspace layout already uses v2.");
|
|
4348
4927
|
return;
|
|
4349
4928
|
}
|
|
4350
4929
|
const identity = await resolveMigrationIdentity(root, options);
|
|
4351
4930
|
const projectId = identity.projectId;
|
|
4352
|
-
const projectRoot =
|
|
4353
|
-
if (
|
|
4354
|
-
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.`);
|
|
4355
4934
|
}
|
|
4356
|
-
const changed = legacyEntries.map((entry) => `${
|
|
4935
|
+
const changed = legacyEntries.map((entry) => `${path17.join(".coop", entry)} -> ${path17.join(".coop", "projects", projectId, entry)}`);
|
|
4357
4936
|
changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
|
|
4358
4937
|
console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
|
|
4359
4938
|
console.log(`- from: v1 flat layout`);
|
|
@@ -4369,21 +4948,21 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4369
4948
|
console.log("- no files were modified.");
|
|
4370
4949
|
return;
|
|
4371
4950
|
}
|
|
4372
|
-
|
|
4373
|
-
|
|
4951
|
+
fs12.mkdirSync(projectsDir, { recursive: true });
|
|
4952
|
+
fs12.mkdirSync(projectRoot, { recursive: true });
|
|
4374
4953
|
for (const entry of legacyEntries) {
|
|
4375
|
-
const source =
|
|
4376
|
-
const destination =
|
|
4377
|
-
if (
|
|
4954
|
+
const source = path17.join(workspaceDir, entry);
|
|
4955
|
+
const destination = path17.join(projectRoot, entry);
|
|
4956
|
+
if (fs12.existsSync(destination)) {
|
|
4378
4957
|
if (!options.force) {
|
|
4379
|
-
throw new Error(`Destination '${
|
|
4958
|
+
throw new Error(`Destination '${path17.relative(root, destination)}' already exists.`);
|
|
4380
4959
|
}
|
|
4381
|
-
|
|
4960
|
+
fs12.rmSync(destination, { recursive: true, force: true });
|
|
4382
4961
|
}
|
|
4383
|
-
|
|
4962
|
+
fs12.renameSync(source, destination);
|
|
4384
4963
|
}
|
|
4385
|
-
const movedConfigPath =
|
|
4386
|
-
if (
|
|
4964
|
+
const movedConfigPath = path17.join(projectRoot, "config.yml");
|
|
4965
|
+
if (fs12.existsSync(movedConfigPath)) {
|
|
4387
4966
|
const movedConfig = parseYamlFile2(movedConfigPath);
|
|
4388
4967
|
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
4389
4968
|
nextProject.name = identity.projectName;
|
|
@@ -4400,13 +4979,13 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
4400
4979
|
}
|
|
4401
4980
|
const workspace = readWorkspaceConfig(root);
|
|
4402
4981
|
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
|
|
4403
|
-
writeIfMissing2(
|
|
4404
|
-
writeIfMissing2(
|
|
4982
|
+
writeIfMissing2(path17.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE2);
|
|
4983
|
+
writeIfMissing2(path17.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE2);
|
|
4405
4984
|
ensureGitignoreEntry2(root, ".coop/logs/");
|
|
4406
4985
|
ensureGitignoreEntry2(root, ".coop/tmp/");
|
|
4407
4986
|
const manager = new IndexManager3(projectRoot);
|
|
4408
4987
|
manager.build_full_index();
|
|
4409
|
-
console.log(`[COOP] migrated workspace to v2 at ${
|
|
4988
|
+
console.log(`[COOP] migrated workspace to v2 at ${path17.relative(root, projectRoot)}`);
|
|
4410
4989
|
}
|
|
4411
4990
|
function registerMigrateCommand(program) {
|
|
4412
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) => {
|
|
@@ -4425,7 +5004,7 @@ function registerMigrateCommand(program) {
|
|
|
4425
5004
|
if (report.changed_files.length > 0) {
|
|
4426
5005
|
console.log("- changed files:");
|
|
4427
5006
|
for (const filePath of report.changed_files) {
|
|
4428
|
-
console.log(` - ${
|
|
5007
|
+
console.log(` - ${path17.relative(root, filePath)}`);
|
|
4429
5008
|
}
|
|
4430
5009
|
}
|
|
4431
5010
|
if (report.dry_run) {
|
|
@@ -4519,7 +5098,7 @@ import chalk3 from "chalk";
|
|
|
4519
5098
|
import {
|
|
4520
5099
|
analyze_feasibility,
|
|
4521
5100
|
analyze_what_if,
|
|
4522
|
-
load_graph as
|
|
5101
|
+
load_graph as load_graph7,
|
|
4523
5102
|
monte_carlo_forecast,
|
|
4524
5103
|
TaskPriority as TaskPriority2
|
|
4525
5104
|
} from "@kitsy/coop-core";
|
|
@@ -4583,8 +5162,19 @@ function collectWhatIfModifications(options) {
|
|
|
4583
5162
|
}
|
|
4584
5163
|
async function runPlanDelivery(deliveryName, options) {
|
|
4585
5164
|
const root = resolveRepoRoot();
|
|
4586
|
-
const
|
|
4587
|
-
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);
|
|
4588
5178
|
const modifications = collectWhatIfModifications(options);
|
|
4589
5179
|
if (modifications.length > 0) {
|
|
4590
5180
|
const comparison = analyze_what_if(
|
|
@@ -4699,7 +5289,7 @@ async function runPlanDelivery(deliveryName, options) {
|
|
|
4699
5289
|
}
|
|
4700
5290
|
function runPlanCapacity(trackArg, options) {
|
|
4701
5291
|
const root = resolveRepoRoot();
|
|
4702
|
-
const graph =
|
|
5292
|
+
const graph = load_graph7(coopDir(root));
|
|
4703
5293
|
const track = normalizeTrack(trackArg);
|
|
4704
5294
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
4705
5295
|
if (deliveries.length === 0) {
|
|
@@ -4739,17 +5329,43 @@ function runPlanCapacity(trackArg, options) {
|
|
|
4739
5329
|
}
|
|
4740
5330
|
function registerPlanCommand(program) {
|
|
4741
5331
|
const plan = program.command("plan").description("Planning commands");
|
|
4742
|
-
plan.command("delivery").description("Analyze delivery feasibility").argument("
|
|
4743
|
-
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);
|
|
4744
5334
|
});
|
|
4745
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) => {
|
|
4746
5336
|
runPlanCapacity(track, options);
|
|
4747
5337
|
});
|
|
4748
5338
|
}
|
|
4749
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
|
+
|
|
4750
5366
|
// src/commands/project.ts
|
|
4751
|
-
import
|
|
4752
|
-
import
|
|
5367
|
+
import fs13 from "fs";
|
|
5368
|
+
import path18 from "path";
|
|
4753
5369
|
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
|
|
4754
5370
|
var TASK_TEMPLATE2 = `---
|
|
4755
5371
|
id: TASK-001
|
|
@@ -4858,11 +5474,11 @@ var PROJECT_DIRS = [
|
|
|
4858
5474
|
".index"
|
|
4859
5475
|
];
|
|
4860
5476
|
function ensureDir2(dirPath) {
|
|
4861
|
-
|
|
5477
|
+
fs13.mkdirSync(dirPath, { recursive: true });
|
|
4862
5478
|
}
|
|
4863
5479
|
function writeIfMissing3(filePath, content) {
|
|
4864
|
-
if (!
|
|
4865
|
-
|
|
5480
|
+
if (!fs13.existsSync(filePath)) {
|
|
5481
|
+
fs13.writeFileSync(filePath, content, "utf8");
|
|
4866
5482
|
}
|
|
4867
5483
|
}
|
|
4868
5484
|
function normalizeProjectId3(value) {
|
|
@@ -4870,17 +5486,17 @@ function normalizeProjectId3(value) {
|
|
|
4870
5486
|
}
|
|
4871
5487
|
function createProject(root, projectId, projectName, namingTemplate = DEFAULT_ID_NAMING_TEMPLATE) {
|
|
4872
5488
|
const workspaceDir = coopWorkspaceDir(root);
|
|
4873
|
-
const projectRoot =
|
|
4874
|
-
ensureDir2(
|
|
5489
|
+
const projectRoot = path18.join(workspaceDir, "projects", projectId);
|
|
5490
|
+
ensureDir2(path18.join(workspaceDir, "projects"));
|
|
4875
5491
|
for (const dir of PROJECT_DIRS) {
|
|
4876
|
-
ensureDir2(
|
|
5492
|
+
ensureDir2(path18.join(projectRoot, dir));
|
|
4877
5493
|
}
|
|
4878
|
-
writeIfMissing3(
|
|
4879
|
-
if (!
|
|
5494
|
+
writeIfMissing3(path18.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName, namingTemplate));
|
|
5495
|
+
if (!fs13.existsSync(path18.join(projectRoot, "schema-version"))) {
|
|
4880
5496
|
write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
|
|
4881
5497
|
}
|
|
4882
|
-
writeIfMissing3(
|
|
4883
|
-
writeIfMissing3(
|
|
5498
|
+
writeIfMissing3(path18.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
|
|
5499
|
+
writeIfMissing3(path18.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
|
|
4884
5500
|
return projectRoot;
|
|
4885
5501
|
}
|
|
4886
5502
|
function registerProjectCommand(program) {
|
|
@@ -4908,7 +5524,7 @@ function registerProjectCommand(program) {
|
|
|
4908
5524
|
}
|
|
4909
5525
|
console.log(`id=${active.id}`);
|
|
4910
5526
|
console.log(`name=${active.name}`);
|
|
4911
|
-
console.log(`path=${
|
|
5527
|
+
console.log(`path=${path18.relative(root, active.root)}`);
|
|
4912
5528
|
console.log(`layout=${active.layout}`);
|
|
4913
5529
|
});
|
|
4914
5530
|
project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
|
|
@@ -4940,26 +5556,127 @@ function registerProjectCommand(program) {
|
|
|
4940
5556
|
version: 2,
|
|
4941
5557
|
current_project: workspace.current_project || projectId
|
|
4942
5558
|
});
|
|
4943
|
-
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());
|
|
4944
5661
|
});
|
|
4945
5662
|
}
|
|
4946
5663
|
|
|
4947
5664
|
// src/commands/refine.ts
|
|
4948
|
-
import
|
|
4949
|
-
import
|
|
4950
|
-
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";
|
|
4951
5668
|
import { create_provider_refinement_client, refine_idea_to_draft, refine_task_to_draft } from "@kitsy/coop-ai";
|
|
4952
|
-
function
|
|
5669
|
+
function resolveTaskFile2(root, idOrAlias) {
|
|
4953
5670
|
const target = resolveReference(root, idOrAlias, "task");
|
|
4954
|
-
return
|
|
5671
|
+
return path19.join(root, ...target.file.split("/"));
|
|
4955
5672
|
}
|
|
4956
|
-
function
|
|
5673
|
+
function resolveIdeaFile3(root, idOrAlias) {
|
|
4957
5674
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
4958
|
-
return
|
|
5675
|
+
return path19.join(root, ...target.file.split("/"));
|
|
4959
5676
|
}
|
|
4960
5677
|
async function readSupplementalInput(root, options) {
|
|
4961
5678
|
if (options.inputFile?.trim()) {
|
|
4962
|
-
return
|
|
5679
|
+
return fs15.readFileSync(path19.resolve(root, options.inputFile.trim()), "utf8");
|
|
4963
5680
|
}
|
|
4964
5681
|
if (options.stdin) {
|
|
4965
5682
|
return readStdinText();
|
|
@@ -4977,11 +5694,11 @@ function loadAuthorityContext(root, refs) {
|
|
|
4977
5694
|
for (const ref of refs ?? []) {
|
|
4978
5695
|
const filePart = extractRefFile(ref);
|
|
4979
5696
|
if (!filePart) continue;
|
|
4980
|
-
const fullPath =
|
|
4981
|
-
if (!
|
|
5697
|
+
const fullPath = path19.resolve(root, filePart);
|
|
5698
|
+
if (!fs15.existsSync(fullPath) || !fs15.statSync(fullPath).isFile()) continue;
|
|
4982
5699
|
out.push({
|
|
4983
5700
|
ref,
|
|
4984
|
-
content:
|
|
5701
|
+
content: fs15.readFileSync(fullPath, "utf8")
|
|
4985
5702
|
});
|
|
4986
5703
|
}
|
|
4987
5704
|
return out;
|
|
@@ -4991,8 +5708,8 @@ function registerRefineCommand(program) {
|
|
|
4991
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) => {
|
|
4992
5709
|
const root = resolveRepoRoot();
|
|
4993
5710
|
const projectDir = ensureCoopInitialized(root);
|
|
4994
|
-
const ideaFile =
|
|
4995
|
-
const parsed =
|
|
5711
|
+
const ideaFile = resolveIdeaFile3(root, id);
|
|
5712
|
+
const parsed = parseIdeaFile5(ideaFile);
|
|
4996
5713
|
const supplemental = await readSupplementalInput(root, options);
|
|
4997
5714
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
4998
5715
|
const refined = await refine_idea_to_draft(
|
|
@@ -5015,8 +5732,8 @@ function registerRefineCommand(program) {
|
|
|
5015
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) => {
|
|
5016
5733
|
const root = resolveRepoRoot();
|
|
5017
5734
|
const projectDir = ensureCoopInitialized(root);
|
|
5018
|
-
const taskFile =
|
|
5019
|
-
const parsed =
|
|
5735
|
+
const taskFile = resolveTaskFile2(root, id);
|
|
5736
|
+
const parsed = parseTaskFile11(taskFile);
|
|
5020
5737
|
const supplemental = await readSupplementalInput(root, options);
|
|
5021
5738
|
const client = create_provider_refinement_client(readCoopConfig(root).raw);
|
|
5022
5739
|
const draft = await refine_task_to_draft(
|
|
@@ -5044,15 +5761,15 @@ function registerRefineCommand(program) {
|
|
|
5044
5761
|
const written = applyRefinementDraft(root, projectDir, draft);
|
|
5045
5762
|
console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
|
|
5046
5763
|
for (const filePath of written) {
|
|
5047
|
-
console.log(`- ${
|
|
5764
|
+
console.log(`- ${path19.relative(root, filePath)}`);
|
|
5048
5765
|
}
|
|
5049
5766
|
});
|
|
5050
5767
|
}
|
|
5051
5768
|
|
|
5052
5769
|
// src/commands/run.ts
|
|
5053
|
-
import
|
|
5054
|
-
import
|
|
5055
|
-
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";
|
|
5056
5773
|
import {
|
|
5057
5774
|
build_contract,
|
|
5058
5775
|
create_provider_agent_client,
|
|
@@ -5061,11 +5778,11 @@ import {
|
|
|
5061
5778
|
} from "@kitsy/coop-ai";
|
|
5062
5779
|
function loadTask(root, idOrAlias) {
|
|
5063
5780
|
const target = resolveReference(root, idOrAlias, "task");
|
|
5064
|
-
const taskFile =
|
|
5065
|
-
if (!
|
|
5781
|
+
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5782
|
+
if (!fs16.existsSync(taskFile)) {
|
|
5066
5783
|
throw new Error(`Task file not found: ${target.file}`);
|
|
5067
5784
|
}
|
|
5068
|
-
return
|
|
5785
|
+
return parseTaskFile12(taskFile).task;
|
|
5069
5786
|
}
|
|
5070
5787
|
function printContract(contract) {
|
|
5071
5788
|
console.log(JSON.stringify(contract, null, 2));
|
|
@@ -5078,7 +5795,7 @@ function registerRunCommand(program) {
|
|
|
5078
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) => {
|
|
5079
5796
|
const root = resolveRepoRoot();
|
|
5080
5797
|
const coop = ensureCoopInitialized(root);
|
|
5081
|
-
const graph =
|
|
5798
|
+
const graph = load_graph8(coopDir(root));
|
|
5082
5799
|
const task = loadTask(root, id);
|
|
5083
5800
|
const config = readCoopConfig(root).raw;
|
|
5084
5801
|
const routedAgent = select_agent(task, config);
|
|
@@ -5101,26 +5818,141 @@ function registerRunCommand(program) {
|
|
|
5101
5818
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
5102
5819
|
});
|
|
5103
5820
|
if (result.status === "failed") {
|
|
5104
|
-
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)}`);
|
|
5105
5822
|
}
|
|
5106
5823
|
if (result.status === "paused") {
|
|
5107
5824
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
5108
|
-
console.log(`[COOP] log: ${
|
|
5825
|
+
console.log(`[COOP] log: ${path20.relative(root, result.log_path)}`);
|
|
5109
5826
|
return;
|
|
5110
5827
|
}
|
|
5111
5828
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
5112
|
-
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
|
+
}
|
|
5113
5945
|
});
|
|
5114
5946
|
}
|
|
5115
5947
|
|
|
5116
5948
|
// src/server/api.ts
|
|
5117
|
-
import
|
|
5949
|
+
import fs17 from "fs";
|
|
5118
5950
|
import http2 from "http";
|
|
5119
|
-
import
|
|
5951
|
+
import path21 from "path";
|
|
5120
5952
|
import {
|
|
5121
5953
|
analyze_feasibility as analyze_feasibility2,
|
|
5122
|
-
load_graph as
|
|
5123
|
-
parseTaskFile as
|
|
5954
|
+
load_graph as load_graph10,
|
|
5955
|
+
parseTaskFile as parseTaskFile14,
|
|
5124
5956
|
resolve_external_dependencies
|
|
5125
5957
|
} from "@kitsy/coop-core";
|
|
5126
5958
|
function json(res, statusCode, payload) {
|
|
@@ -5162,12 +5994,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
5162
5994
|
};
|
|
5163
5995
|
}
|
|
5164
5996
|
function taskFileById(root, id) {
|
|
5165
|
-
const tasksDir =
|
|
5166
|
-
if (!
|
|
5167
|
-
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 });
|
|
5168
6000
|
const target = `${id}.md`.toLowerCase();
|
|
5169
6001
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
5170
|
-
return match ?
|
|
6002
|
+
return match ? path21.join(tasksDir, match.name) : null;
|
|
5171
6003
|
}
|
|
5172
6004
|
function loadRemoteConfig(root) {
|
|
5173
6005
|
const raw = readCoopConfig(root).raw;
|
|
@@ -5233,7 +6065,7 @@ function createApiServer(root, options = {}) {
|
|
|
5233
6065
|
}
|
|
5234
6066
|
const project = resolveProject(repoRoot);
|
|
5235
6067
|
const coopPath = project.root;
|
|
5236
|
-
const graph =
|
|
6068
|
+
const graph = load_graph10(coopPath);
|
|
5237
6069
|
const resolutions = await externalResolutions(repoRoot, graph, options);
|
|
5238
6070
|
if (pathname === "/api/tasks") {
|
|
5239
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) ?? []));
|
|
@@ -5248,7 +6080,7 @@ function createApiServer(root, options = {}) {
|
|
|
5248
6080
|
notFound(res, `Task '${taskId}' not found.`);
|
|
5249
6081
|
return;
|
|
5250
6082
|
}
|
|
5251
|
-
const parsed =
|
|
6083
|
+
const parsed = parseTaskFile14(filePath);
|
|
5252
6084
|
const task = graph.nodes.get(parsed.task.id) ?? parsed.task;
|
|
5253
6085
|
json(res, 200, {
|
|
5254
6086
|
...workspaceMeta(repoRoot),
|
|
@@ -5257,7 +6089,7 @@ function createApiServer(root, options = {}) {
|
|
|
5257
6089
|
created: task.created,
|
|
5258
6090
|
updated: task.updated,
|
|
5259
6091
|
body: parsed.body,
|
|
5260
|
-
file_path:
|
|
6092
|
+
file_path: path21.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
5261
6093
|
}
|
|
5262
6094
|
});
|
|
5263
6095
|
return;
|
|
@@ -5345,9 +6177,9 @@ function registerServeCommand(program) {
|
|
|
5345
6177
|
}
|
|
5346
6178
|
|
|
5347
6179
|
// src/commands/show.ts
|
|
5348
|
-
import
|
|
5349
|
-
import
|
|
5350
|
-
import { parseIdeaFile as
|
|
6180
|
+
import fs18 from "fs";
|
|
6181
|
+
import path22 from "path";
|
|
6182
|
+
import { parseIdeaFile as parseIdeaFile7 } from "@kitsy/coop-core";
|
|
5351
6183
|
function stringify(value) {
|
|
5352
6184
|
if (value === null || value === void 0) return "-";
|
|
5353
6185
|
if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "-";
|
|
@@ -5365,13 +6197,13 @@ function pushListSection(lines, title, values) {
|
|
|
5365
6197
|
}
|
|
5366
6198
|
}
|
|
5367
6199
|
function loadComputedFromIndex(root, taskId) {
|
|
5368
|
-
const indexPath =
|
|
5369
|
-
if (!
|
|
6200
|
+
const indexPath = path22.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
6201
|
+
if (!fs18.existsSync(indexPath)) {
|
|
5370
6202
|
return null;
|
|
5371
6203
|
}
|
|
5372
6204
|
let parsed;
|
|
5373
6205
|
try {
|
|
5374
|
-
parsed = JSON.parse(
|
|
6206
|
+
parsed = JSON.parse(fs18.readFileSync(indexPath, "utf8"));
|
|
5375
6207
|
} catch {
|
|
5376
6208
|
return null;
|
|
5377
6209
|
}
|
|
@@ -5406,12 +6238,16 @@ function loadComputedFromIndex(root, taskId) {
|
|
|
5406
6238
|
}
|
|
5407
6239
|
return null;
|
|
5408
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
|
+
}
|
|
5409
6247
|
function showTask(taskId) {
|
|
5410
6248
|
const root = resolveRepoRoot();
|
|
5411
|
-
const
|
|
5412
|
-
const
|
|
5413
|
-
const taskFile = path20.join(root, ...target.file.split("/"));
|
|
5414
|
-
const parsed = parseTaskFile13(taskFile);
|
|
6249
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
6250
|
+
const { filePath, parsed } = loadTaskEntry(root, taskId);
|
|
5415
6251
|
const task = parsed.task;
|
|
5416
6252
|
const body = parsed.body.trim();
|
|
5417
6253
|
const computed = loadComputedFromIndex(root, task.id);
|
|
@@ -5421,15 +6257,22 @@ function showTask(taskId) {
|
|
|
5421
6257
|
`Status: ${task.status}`,
|
|
5422
6258
|
`Type: ${task.type}`,
|
|
5423
6259
|
`Priority: ${task.priority ?? "-"}`,
|
|
6260
|
+
`Effective Priority: ${taskEffectivePriority(task, context.track)}`,
|
|
5424
6261
|
`Track: ${task.track ?? "-"}`,
|
|
6262
|
+
`Delivery Tracks: ${stringify(task.delivery_tracks)}`,
|
|
6263
|
+
`Priority Context: ${stringify(task.priority_context)}`,
|
|
5425
6264
|
`Assignee: ${task.assignee ?? "-"}`,
|
|
5426
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)}`,
|
|
5427
6270
|
`Aliases: ${stringify(task.aliases)}`,
|
|
5428
6271
|
`Depends On: ${stringify(task.depends_on)}`,
|
|
5429
6272
|
`Tags: ${stringify(task.tags)}`,
|
|
5430
6273
|
`Created: ${task.created}`,
|
|
5431
6274
|
`Updated: ${task.updated}`,
|
|
5432
|
-
`File: ${
|
|
6275
|
+
`File: ${path22.relative(root, filePath)}`,
|
|
5433
6276
|
""
|
|
5434
6277
|
];
|
|
5435
6278
|
pushListSection(lines, "Acceptance", task.acceptance);
|
|
@@ -5446,15 +6289,25 @@ function showTask(taskId) {
|
|
|
5446
6289
|
lines.push(`- Promoted To: ${stringify(task.origin.promoted_to)}`);
|
|
5447
6290
|
lines.push(`- Snapshot SHA256: ${task.origin.snapshot_sha256 ?? "-"}`);
|
|
5448
6291
|
}
|
|
5449
|
-
lines.push(
|
|
5450
|
-
|
|
5451
|
-
"
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
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:");
|
|
5456
6309
|
if (!computed) {
|
|
5457
|
-
lines.push(`index not built (${
|
|
6310
|
+
lines.push(`index not built (${path22.relative(root, path22.join(ensureCoopInitialized(root), ".index", "tasks.json"))} missing)`);
|
|
5458
6311
|
} else {
|
|
5459
6312
|
for (const [key, value] of Object.entries(computed)) {
|
|
5460
6313
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -5464,10 +6317,9 @@ function showTask(taskId) {
|
|
|
5464
6317
|
}
|
|
5465
6318
|
function showIdea(ideaId) {
|
|
5466
6319
|
const root = resolveRepoRoot();
|
|
5467
|
-
ensureCoopInitialized(root);
|
|
5468
6320
|
const target = resolveReference(root, ideaId, "idea");
|
|
5469
|
-
const ideaFile =
|
|
5470
|
-
const parsed =
|
|
6321
|
+
const ideaFile = path22.join(root, ...target.file.split("/"));
|
|
6322
|
+
const parsed = parseIdeaFile7(ideaFile);
|
|
5471
6323
|
const idea = parsed.idea;
|
|
5472
6324
|
const body = parsed.body.trim();
|
|
5473
6325
|
const lines = [
|
|
@@ -5480,29 +6332,68 @@ function showIdea(ideaId) {
|
|
|
5480
6332
|
`Tags: ${stringify(idea.tags)}`,
|
|
5481
6333
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
5482
6334
|
`Created: ${idea.created}`,
|
|
5483
|
-
`File: ${
|
|
6335
|
+
`File: ${path22.relative(root, ideaFile)}`,
|
|
5484
6336
|
"",
|
|
5485
6337
|
"Body:",
|
|
5486
6338
|
body || "-"
|
|
5487
6339
|
];
|
|
5488
6340
|
console.log(lines.join("\n"));
|
|
5489
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
|
+
}
|
|
5490
6376
|
function registerShowCommand(program) {
|
|
5491
|
-
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
|
+
});
|
|
5492
6383
|
show.command("task").description("Show task details").argument("<id>", "Task ID").action((id) => {
|
|
5493
6384
|
showTask(id);
|
|
5494
6385
|
});
|
|
5495
6386
|
show.command("idea").description("Show idea details").argument("<id>", "Idea ID").action((id) => {
|
|
5496
6387
|
showIdea(id);
|
|
5497
6388
|
});
|
|
5498
|
-
show.command("delivery").description("Show delivery details
|
|
5499
|
-
|
|
6389
|
+
show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").action((id) => {
|
|
6390
|
+
showDelivery(id);
|
|
5500
6391
|
});
|
|
5501
6392
|
}
|
|
5502
6393
|
|
|
5503
6394
|
// src/commands/status.ts
|
|
5504
6395
|
import chalk4 from "chalk";
|
|
5505
|
-
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";
|
|
5506
6397
|
function countBy(values, keyFn) {
|
|
5507
6398
|
const out = /* @__PURE__ */ new Map();
|
|
5508
6399
|
for (const value of values) {
|
|
@@ -5523,10 +6414,16 @@ function completionSummary(done, totalTasks) {
|
|
|
5523
6414
|
const percent = totalTasks > 0 ? Math.round(done / totalTasks * 100) : 0;
|
|
5524
6415
|
return `${done}/${totalTasks} (${percent}%)`;
|
|
5525
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
|
+
}
|
|
5526
6423
|
function registerStatusCommand(program) {
|
|
5527
6424
|
program.command("status").description("Project dashboard overview").option("--today <date>", "Evaluation date (YYYY-MM-DD)").action((options) => {
|
|
5528
6425
|
const root = resolveRepoRoot();
|
|
5529
|
-
const graph =
|
|
6426
|
+
const graph = load_graph11(coopDir(root));
|
|
5530
6427
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
5531
6428
|
const tasks = Array.from(graph.nodes.values());
|
|
5532
6429
|
const tasksByStatus = countBy(tasks, (task) => task.status);
|
|
@@ -5546,6 +6443,14 @@ function registerStatusCommand(program) {
|
|
|
5546
6443
|
const trackRows = Array.from(tasksByTrack.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([track, count]) => [track, String(count)]);
|
|
5547
6444
|
lines.push(formatTable(["Track", "Count"], trackRows));
|
|
5548
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("");
|
|
5549
6454
|
lines.push(chalk4.bold("Delivery Health"));
|
|
5550
6455
|
const deliveryRows = [];
|
|
5551
6456
|
const riskLines = [];
|
|
@@ -5597,8 +6502,8 @@ function registerTransitionCommand(program) {
|
|
|
5597
6502
|
}
|
|
5598
6503
|
|
|
5599
6504
|
// src/commands/taskflow.ts
|
|
5600
|
-
import
|
|
5601
|
-
import { parseTaskFile as
|
|
6505
|
+
import path23 from "path";
|
|
6506
|
+
import { parseTaskFile as parseTaskFile15 } from "@kitsy/coop-core";
|
|
5602
6507
|
function normalizeExecutor(value) {
|
|
5603
6508
|
if (!value?.trim()) return void 0;
|
|
5604
6509
|
const normalized = value.trim().toLowerCase();
|
|
@@ -5635,8 +6540,8 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5635
6540
|
}
|
|
5636
6541
|
}
|
|
5637
6542
|
const reference = resolveReference(root, taskId, "task");
|
|
5638
|
-
const filePath =
|
|
5639
|
-
const parsed =
|
|
6543
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
6544
|
+
const parsed = parseTaskFile15(filePath);
|
|
5640
6545
|
if (parsed.task.status === "in_progress") {
|
|
5641
6546
|
console.log(`Task ${parsed.task.id} is already in_progress.`);
|
|
5642
6547
|
return;
|
|
@@ -5648,10 +6553,37 @@ async function claimAndStart(root, taskId, options) {
|
|
|
5648
6553
|
});
|
|
5649
6554
|
console.log(`Updated ${transitioned.task.id}: ${transitioned.from} -> ${transitioned.to}`);
|
|
5650
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
|
+
}
|
|
5651
6582
|
function registerTaskFlowCommands(program) {
|
|
5652
6583
|
const next = program.command("next").description("Select the next COOP work item");
|
|
5653
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) => {
|
|
5654
6585
|
const root = resolveRepoRoot();
|
|
6586
|
+
printResolvedSelectionContext(root, options);
|
|
5655
6587
|
const selected = selectTopReadyTask(root, {
|
|
5656
6588
|
track: options.track,
|
|
5657
6589
|
delivery: options.delivery,
|
|
@@ -5661,29 +6593,49 @@ function registerTaskFlowCommands(program) {
|
|
|
5661
6593
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
5662
6594
|
});
|
|
5663
6595
|
const pick = program.command("pick").description("Pick the next COOP work item");
|
|
5664
|
-
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) => {
|
|
5665
6597
|
const root = resolveRepoRoot();
|
|
5666
|
-
|
|
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, {
|
|
5667
6615
|
track: options.track,
|
|
5668
6616
|
delivery: options.delivery,
|
|
6617
|
+
version: options.version,
|
|
5669
6618
|
executor: normalizeExecutor(options.executor),
|
|
5670
6619
|
today: options.today
|
|
5671
6620
|
});
|
|
5672
6621
|
console.log(formatSelectedTask(selected.entry, selected.selection));
|
|
6622
|
+
maybePromote(root, selected.entry.task.id, options);
|
|
5673
6623
|
await claimAndStart(root, selected.entry.task.id, options);
|
|
5674
6624
|
});
|
|
5675
6625
|
const start = program.command("start").description("Start COOP work on a task");
|
|
5676
|
-
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) => {
|
|
5677
6627
|
const root = resolveRepoRoot();
|
|
6628
|
+
printResolvedSelectionContext(root, options);
|
|
5678
6629
|
const taskId = id?.trim() || selectTopReadyTask(root, {
|
|
5679
6630
|
track: options.track,
|
|
5680
6631
|
delivery: options.delivery,
|
|
6632
|
+
version: options.version,
|
|
5681
6633
|
executor: normalizeExecutor(options.executor),
|
|
5682
6634
|
today: options.today
|
|
5683
6635
|
}).entry.task.id;
|
|
5684
6636
|
const reference = resolveReference(root, taskId, "task");
|
|
5685
|
-
const filePath =
|
|
5686
|
-
const parsed =
|
|
6637
|
+
const filePath = path23.join(root, ...reference.file.split("/"));
|
|
6638
|
+
const parsed = parseTaskFile15(filePath);
|
|
5687
6639
|
console.log(
|
|
5688
6640
|
formatSelectedTask(
|
|
5689
6641
|
{
|
|
@@ -5696,29 +6648,31 @@ function registerTaskFlowCommands(program) {
|
|
|
5696
6648
|
{
|
|
5697
6649
|
track: options.track,
|
|
5698
6650
|
delivery: options.delivery,
|
|
6651
|
+
version: options.version,
|
|
5699
6652
|
executor: normalizeExecutor(options.executor),
|
|
5700
6653
|
today: options.today
|
|
5701
6654
|
}
|
|
5702
6655
|
)
|
|
5703
6656
|
);
|
|
6657
|
+
maybePromote(root, reference.id, options);
|
|
5704
6658
|
await claimAndStart(root, reference.id, options);
|
|
5705
6659
|
});
|
|
5706
6660
|
}
|
|
5707
6661
|
|
|
5708
6662
|
// src/commands/ui.ts
|
|
5709
|
-
import
|
|
5710
|
-
import
|
|
6663
|
+
import fs19 from "fs";
|
|
6664
|
+
import path24 from "path";
|
|
5711
6665
|
import { createRequire } from "module";
|
|
5712
6666
|
import { fileURLToPath } from "url";
|
|
5713
6667
|
import { spawn } from "child_process";
|
|
5714
6668
|
import { IndexManager as IndexManager4 } from "@kitsy/coop-core";
|
|
5715
6669
|
function findPackageRoot(entryPath) {
|
|
5716
|
-
let current =
|
|
6670
|
+
let current = path24.dirname(entryPath);
|
|
5717
6671
|
while (true) {
|
|
5718
|
-
if (
|
|
6672
|
+
if (fs19.existsSync(path24.join(current, "package.json"))) {
|
|
5719
6673
|
return current;
|
|
5720
6674
|
}
|
|
5721
|
-
const parent =
|
|
6675
|
+
const parent = path24.dirname(current);
|
|
5722
6676
|
if (parent === current) {
|
|
5723
6677
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
5724
6678
|
}
|
|
@@ -5767,9 +6721,9 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
5767
6721
|
const project = resolveProject(repoRoot);
|
|
5768
6722
|
ensureIndex(repoRoot);
|
|
5769
6723
|
const uiRoot = resolveUiPackageRoot();
|
|
5770
|
-
const requireFromUi = createRequire(
|
|
6724
|
+
const requireFromUi = createRequire(path24.join(uiRoot, "package.json"));
|
|
5771
6725
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
5772
|
-
const viteBin =
|
|
6726
|
+
const viteBin = path24.join(path24.dirname(vitePackageJson), "bin", "vite.js");
|
|
5773
6727
|
const url = `http://${host}:${port}`;
|
|
5774
6728
|
console.log(`COOP UI: ${url}`);
|
|
5775
6729
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -5815,12 +6769,197 @@ function registerUiCommand(program) {
|
|
|
5815
6769
|
});
|
|
5816
6770
|
}
|
|
5817
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
|
+
|
|
5818
6957
|
// src/commands/view.ts
|
|
5819
6958
|
import {
|
|
5820
6959
|
analyze_feasibility as analyze_feasibility4,
|
|
5821
6960
|
compute_velocity,
|
|
5822
6961
|
load_completed_runs,
|
|
5823
|
-
load_graph as
|
|
6962
|
+
load_graph as load_graph12,
|
|
5824
6963
|
simulate_schedule
|
|
5825
6964
|
} from "@kitsy/coop-core";
|
|
5826
6965
|
var STATUS_COLUMNS = [
|
|
@@ -5883,9 +7022,12 @@ function formatAccuracy(value) {
|
|
|
5883
7022
|
if (value == null) return "-";
|
|
5884
7023
|
return `${Math.round(value * 100)}%`;
|
|
5885
7024
|
}
|
|
7025
|
+
function workedHours2(task) {
|
|
7026
|
+
return (task.time?.logs ?? []).filter((entry) => entry.kind === "worked").reduce((sum, entry) => sum + entry.hours, 0);
|
|
7027
|
+
}
|
|
5886
7028
|
function runKanban() {
|
|
5887
7029
|
const root = resolveRepoRoot();
|
|
5888
|
-
const graph =
|
|
7030
|
+
const graph = load_graph12(coopDir(root));
|
|
5889
7031
|
const grouped = /* @__PURE__ */ new Map();
|
|
5890
7032
|
for (const status of STATUS_COLUMNS) {
|
|
5891
7033
|
grouped.set(status, []);
|
|
@@ -5917,11 +7059,19 @@ function runKanban() {
|
|
|
5917
7059
|
}
|
|
5918
7060
|
function runTimeline(options) {
|
|
5919
7061
|
const root = resolveRepoRoot();
|
|
5920
|
-
const
|
|
5921
|
-
|
|
5922
|
-
|
|
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>`.");
|
|
5923
7073
|
}
|
|
5924
|
-
const delivery = resolveDelivery(graph,
|
|
7074
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
5925
7075
|
const include = new Set(delivery.scope.include);
|
|
5926
7076
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
5927
7077
|
const tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -5962,7 +7112,7 @@ function runTimeline(options) {
|
|
|
5962
7112
|
function runVelocity(options) {
|
|
5963
7113
|
const root = resolveRepoRoot();
|
|
5964
7114
|
const coopPath = coopDir(root);
|
|
5965
|
-
const graph =
|
|
7115
|
+
const graph = load_graph12(coopPath);
|
|
5966
7116
|
const config = readCoopConfig(root).raw;
|
|
5967
7117
|
const scheduling = typeof config.scheduling === "object" && config.scheduling !== null ? config.scheduling : {};
|
|
5968
7118
|
const configuredWeeks = Number(scheduling.velocity_window_weeks);
|
|
@@ -5972,12 +7122,19 @@ function runVelocity(options) {
|
|
|
5972
7122
|
today: options.today ?? /* @__PURE__ */ new Date(),
|
|
5973
7123
|
graph
|
|
5974
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);
|
|
5975
7130
|
console.log(`Velocity: last ${metrics.window_weeks} weeks`);
|
|
5976
7131
|
console.log(`Trend: ${metrics.trend}`);
|
|
5977
7132
|
console.log(`Completed runs: ${metrics.completed_runs}`);
|
|
5978
7133
|
console.log(`Tasks/week: ${metrics.tasks_completed_per_week}`);
|
|
5979
7134
|
console.log(`Hours/week: ${metrics.hours_delivered_per_week}`);
|
|
7135
|
+
console.log(`Story points/week: ${Number((storyPointsTotal / metrics.window_weeks).toFixed(2))}`);
|
|
5980
7136
|
console.log(`Accuracy: ${formatAccuracy(metrics.accuracy_ratio)}`);
|
|
7137
|
+
console.log(`Planned vs Worked: ${plannedHoursTotal.toFixed(2)}h planned | ${workedHoursTotal.toFixed(2)}h worked`);
|
|
5981
7138
|
console.log(`Sparkline: ${sparkline(metrics.points.map((point) => point.completed_tasks))}`);
|
|
5982
7139
|
console.log("");
|
|
5983
7140
|
console.log(
|
|
@@ -5993,12 +7150,20 @@ function runVelocity(options) {
|
|
|
5993
7150
|
}
|
|
5994
7151
|
function runBurndown(options) {
|
|
5995
7152
|
const root = resolveRepoRoot();
|
|
7153
|
+
const context = readWorkingContext(root, resolveCoopHome());
|
|
5996
7154
|
const coopPath = coopDir(root);
|
|
5997
|
-
const graph =
|
|
5998
|
-
|
|
5999
|
-
|
|
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>`.");
|
|
6000
7165
|
}
|
|
6001
|
-
const delivery = resolveDelivery(graph,
|
|
7166
|
+
const delivery = resolveDelivery(graph, deliveryRef);
|
|
6002
7167
|
const include = new Set(delivery.scope.include);
|
|
6003
7168
|
for (const id of delivery.scope.exclude) include.delete(id);
|
|
6004
7169
|
const scopedTasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
@@ -6038,7 +7203,7 @@ function runBurndown(options) {
|
|
|
6038
7203
|
}
|
|
6039
7204
|
function runCapacity(options) {
|
|
6040
7205
|
const root = resolveRepoRoot();
|
|
6041
|
-
const graph =
|
|
7206
|
+
const graph = load_graph12(coopDir(root));
|
|
6042
7207
|
const deliveries = Array.from(graph.deliveries.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
6043
7208
|
if (deliveries.length === 0) {
|
|
6044
7209
|
console.log("No deliveries found.");
|
|
@@ -6126,11 +7291,11 @@ function registerWebhookCommand(program) {
|
|
|
6126
7291
|
}
|
|
6127
7292
|
|
|
6128
7293
|
// src/merge-driver/merge-driver.ts
|
|
6129
|
-
import
|
|
7294
|
+
import fs21 from "fs";
|
|
6130
7295
|
import os2 from "os";
|
|
6131
|
-
import
|
|
7296
|
+
import path25 from "path";
|
|
6132
7297
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6133
|
-
import { stringifyFrontmatter as
|
|
7298
|
+
import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
|
|
6134
7299
|
var STATUS_RANK = {
|
|
6135
7300
|
blocked: 0,
|
|
6136
7301
|
canceled: 0,
|
|
@@ -6211,33 +7376,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
|
|
|
6211
7376
|
return { ok: false, output: stdout };
|
|
6212
7377
|
}
|
|
6213
7378
|
function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
6214
|
-
const ancestorRaw =
|
|
6215
|
-
const oursRaw =
|
|
6216
|
-
const theirsRaw =
|
|
7379
|
+
const ancestorRaw = fs21.readFileSync(ancestorPath, "utf8");
|
|
7380
|
+
const oursRaw = fs21.readFileSync(oursPath, "utf8");
|
|
7381
|
+
const theirsRaw = fs21.readFileSync(theirsPath, "utf8");
|
|
6217
7382
|
const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
|
|
6218
7383
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
6219
7384
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
6220
7385
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
6221
|
-
const tempDir =
|
|
7386
|
+
const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
|
|
6222
7387
|
try {
|
|
6223
|
-
const ancestorBody =
|
|
6224
|
-
const oursBody =
|
|
6225
|
-
const theirsBody =
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
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");
|
|
6229
7394
|
const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
|
|
6230
|
-
const output3 =
|
|
6231
|
-
|
|
7395
|
+
const output3 = stringifyFrontmatter6(mergedFrontmatter, mergedBody.output);
|
|
7396
|
+
fs21.writeFileSync(oursPath, output3, "utf8");
|
|
6232
7397
|
return mergedBody.ok ? 0 : 1;
|
|
6233
7398
|
} finally {
|
|
6234
|
-
|
|
7399
|
+
fs21.rmSync(tempDir, { recursive: true, force: true });
|
|
6235
7400
|
}
|
|
6236
7401
|
}
|
|
6237
7402
|
function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
6238
|
-
const ancestor = parseYamlContent3(
|
|
6239
|
-
const ours = parseYamlContent3(
|
|
6240
|
-
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);
|
|
6241
7406
|
const oursUpdated = asTimestamp(ours.updated);
|
|
6242
7407
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
6243
7408
|
const base = ancestor;
|
|
@@ -6247,7 +7412,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
|
6247
7412
|
const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
6248
7413
|
if (value !== void 0) merged[key] = value;
|
|
6249
7414
|
}
|
|
6250
|
-
|
|
7415
|
+
fs21.writeFileSync(oursPath, stringifyYamlContent2(merged), "utf8");
|
|
6251
7416
|
return 0;
|
|
6252
7417
|
}
|
|
6253
7418
|
function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
@@ -6257,12 +7422,17 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
6257
7422
|
return mergeTaskFile(ancestorPath, oursPath, theirsPath);
|
|
6258
7423
|
}
|
|
6259
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
|
+
|
|
6260
7430
|
// src/index.ts
|
|
6261
7431
|
function readVersion() {
|
|
6262
7432
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
6263
|
-
const packageJsonPath =
|
|
7433
|
+
const packageJsonPath = path26.resolve(path26.dirname(currentFile), "..", "package.json");
|
|
6264
7434
|
try {
|
|
6265
|
-
const parsed = JSON.parse(
|
|
7435
|
+
const parsed = JSON.parse(fs22.readFileSync(packageJsonPath, "utf8"));
|
|
6266
7436
|
return parsed.version ?? "0.0.0";
|
|
6267
7437
|
} catch {
|
|
6268
7438
|
return "0.0.0";
|
|
@@ -6282,9 +7452,12 @@ function createProgram() {
|
|
|
6282
7452
|
program.option("-p, --project <id>", "Select the active COOP project");
|
|
6283
7453
|
registerInitCommand(program);
|
|
6284
7454
|
registerCreateCommand(program);
|
|
7455
|
+
registerCurrentCommand(program);
|
|
6285
7456
|
registerAssignCommand(program);
|
|
7457
|
+
registerCommentCommand(program);
|
|
6286
7458
|
registerAliasCommand(program);
|
|
6287
7459
|
registerConfigCommand(program);
|
|
7460
|
+
registerDepsCommand(program);
|
|
6288
7461
|
registerListCommand(program);
|
|
6289
7462
|
registerShowCommand(program);
|
|
6290
7463
|
registerTransitionCommand(program);
|
|
@@ -6293,16 +7466,22 @@ function createProgram() {
|
|
|
6293
7466
|
registerHelpAiCommand(program);
|
|
6294
7467
|
registerIndexCommand(program);
|
|
6295
7468
|
registerLogCommand(program);
|
|
7469
|
+
registerLogTimeCommand(program);
|
|
6296
7470
|
registerLifecycleCommands(program);
|
|
6297
7471
|
registerMigrateCommand(program);
|
|
6298
7472
|
registerNamingCommand(program);
|
|
6299
7473
|
registerPlanCommand(program);
|
|
7474
|
+
registerPromoteCommand(program);
|
|
6300
7475
|
registerProjectCommand(program);
|
|
7476
|
+
registerPromptCommand(program);
|
|
6301
7477
|
registerRefineCommand(program);
|
|
6302
7478
|
registerRunCommand(program);
|
|
7479
|
+
registerSearchCommand(program);
|
|
6303
7480
|
registerServeCommand(program);
|
|
6304
7481
|
registerStatusCommand(program);
|
|
6305
7482
|
registerUiCommand(program);
|
|
7483
|
+
registerUpdateCommand(program);
|
|
7484
|
+
registerUseCommand(program);
|
|
6306
7485
|
registerViewCommand(program);
|
|
6307
7486
|
registerWebhookCommand(program);
|
|
6308
7487
|
registerPhasePlaceholder(program, "ext", 3, "Plugin extension commands");
|
|
@@ -6362,7 +7541,7 @@ async function runCli(argv = process.argv) {
|
|
|
6362
7541
|
function isMainModule() {
|
|
6363
7542
|
const entry = process.argv[1];
|
|
6364
7543
|
if (!entry) return false;
|
|
6365
|
-
return
|
|
7544
|
+
return path26.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
6366
7545
|
}
|
|
6367
7546
|
if (isMainModule()) {
|
|
6368
7547
|
await runCli(process.argv);
|