@kitsy/coop 2.1.2 → 2.2.1

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