@kitsy/coop 2.2.1 → 2.2.2

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 +238 -90
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1391,6 +1391,7 @@ import fs3 from "fs";
1391
1391
  import path3 from "path";
1392
1392
  import {
1393
1393
  effective_priority,
1394
+ parseYamlFile as parseYamlFile2,
1394
1395
  parseDeliveryFile,
1395
1396
  parseIdeaFile as parseIdeaFile2,
1396
1397
  parseTaskFile as parseTaskFile4,
@@ -1399,6 +1400,76 @@ import {
1399
1400
  validateSemantic,
1400
1401
  writeTask as writeTask3
1401
1402
  } from "@kitsy/coop-core";
1403
+ function existingTrackMap(root) {
1404
+ const map = /* @__PURE__ */ new Map();
1405
+ for (const filePath of listTrackFiles(root)) {
1406
+ const track = parseYamlFile2(filePath);
1407
+ if (track.id?.trim()) {
1408
+ map.set(track.id.trim().toLowerCase(), track.id.trim());
1409
+ }
1410
+ }
1411
+ return map;
1412
+ }
1413
+ function existingDeliveryMap(root) {
1414
+ const map = /* @__PURE__ */ new Map();
1415
+ for (const filePath of listDeliveryFiles(root)) {
1416
+ const delivery = parseDeliveryFile(filePath).delivery;
1417
+ if (delivery.id?.trim()) {
1418
+ map.set(delivery.id.trim().toLowerCase(), delivery.id.trim());
1419
+ }
1420
+ }
1421
+ return map;
1422
+ }
1423
+ function resolveExistingTrackId(root, trackId) {
1424
+ const normalized = trackId.trim().toLowerCase();
1425
+ if (!normalized) return void 0;
1426
+ if (normalized === "unassigned") return "unassigned";
1427
+ return existingTrackMap(root).get(normalized);
1428
+ }
1429
+ function resolveExistingDeliveryId(root, deliveryId) {
1430
+ const normalized = deliveryId.trim().toLowerCase();
1431
+ if (!normalized) return void 0;
1432
+ return existingDeliveryMap(root).get(normalized);
1433
+ }
1434
+ function assertExistingTrackId(root, trackId) {
1435
+ const resolved = resolveExistingTrackId(root, trackId);
1436
+ if (resolved) {
1437
+ return resolved;
1438
+ }
1439
+ throw new Error(
1440
+ `Unknown track '${trackId}'. Create it first with \`coop create track --id ${trackId} --name "${trackId}"\` or use \`unassigned\`.`
1441
+ );
1442
+ }
1443
+ function assertExistingDeliveryId(root, deliveryId) {
1444
+ const resolved = resolveExistingDeliveryId(root, deliveryId);
1445
+ if (resolved) {
1446
+ return resolved;
1447
+ }
1448
+ throw new Error(`Unknown delivery '${deliveryId}'. Create it first with \`coop create delivery --id ${deliveryId} --name "${deliveryId}"\`.`);
1449
+ }
1450
+ function unique(values) {
1451
+ return Array.from(new Set(values));
1452
+ }
1453
+ function normalizeTaskReferences(root, task) {
1454
+ const next = { ...task };
1455
+ if (next.track?.trim()) {
1456
+ next.track = assertExistingTrackId(root, next.track.trim());
1457
+ }
1458
+ if (next.delivery?.trim()) {
1459
+ next.delivery = assertExistingDeliveryId(root, next.delivery.trim());
1460
+ }
1461
+ if (next.delivery_tracks?.length) {
1462
+ next.delivery_tracks = unique(next.delivery_tracks.map((trackId) => assertExistingTrackId(root, trackId)));
1463
+ }
1464
+ if (next.priority_context && typeof next.priority_context === "object") {
1465
+ const normalized = {};
1466
+ for (const [trackId, priority] of Object.entries(next.priority_context)) {
1467
+ normalized[assertExistingTrackId(root, trackId)] = priority;
1468
+ }
1469
+ next.priority_context = normalized;
1470
+ }
1471
+ return next;
1472
+ }
1402
1473
  function resolveTaskFile(root, idOrAlias) {
1403
1474
  const reference = resolveReference(root, idOrAlias, "task");
1404
1475
  return path3.join(root, ...reference.file.split("/"));
@@ -1419,17 +1490,19 @@ function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
1419
1490
  const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
1420
1491
  fs3.writeFileSync(filePath, output2, "utf8");
1421
1492
  }
1422
- function validateTaskForWrite(task, filePath) {
1423
- const structuralIssues = validateStructural2(task, { filePath });
1424
- const semanticIssues = validateSemantic(task);
1493
+ function validateTaskForWrite(root, task, filePath) {
1494
+ const normalized = normalizeTaskReferences(root, task);
1495
+ const structuralIssues = validateStructural2(normalized, { filePath });
1496
+ const semanticIssues = validateSemantic(normalized);
1425
1497
  const errors = [...structuralIssues, ...semanticIssues].filter((issue) => issue.level === "error");
1426
1498
  if (errors.length > 0) {
1427
1499
  throw new Error(errors.map((issue) => `- ${issue.message}`).join("\n"));
1428
1500
  }
1501
+ return normalized;
1429
1502
  }
1430
- function writeTaskEntry(filePath, parsed, task, body = parsed.body) {
1431
- validateTaskForWrite(task, filePath);
1432
- writeTask3(task, {
1503
+ function writeTaskEntry(root, filePath, parsed, task, body = parsed.body) {
1504
+ const normalized = validateTaskForWrite(root, task, filePath);
1505
+ writeTask3(normalized, {
1433
1506
  body,
1434
1507
  raw: parsed.raw,
1435
1508
  filePath
@@ -1613,10 +1686,12 @@ function configDefaultVersion(root) {
1613
1686
  }
1614
1687
  function resolveSelectionOptions(root, options) {
1615
1688
  const context = readWorkingContext(root, resolveCoopHome());
1689
+ const track = options.track?.trim() || context.track?.trim() || configDefaultTrack(root);
1690
+ const delivery = options.delivery?.trim() || context.delivery?.trim() || configDefaultDelivery(root);
1616
1691
  return {
1617
1692
  ...options,
1618
- track: options.track?.trim() || context.track?.trim() || configDefaultTrack(root),
1619
- delivery: options.delivery?.trim() || context.delivery?.trim() || configDefaultDelivery(root),
1693
+ track: track ? resolveExistingTrackId(root, track) ?? track : void 0,
1694
+ delivery: delivery ? resolveExistingDeliveryId(root, delivery) ?? delivery : void 0,
1620
1695
  version: options.version?.trim() || context.version?.trim() || configDefaultVersion(root)
1621
1696
  };
1622
1697
  }
@@ -1885,7 +1960,7 @@ function registerCommentCommand(program) {
1885
1960
  const root = resolveRepoRoot();
1886
1961
  const { filePath, parsed } = loadTaskEntry(root, id);
1887
1962
  const task = appendTaskComment(parsed.task, options.author?.trim() || defaultCoopAuthor(root), options.message.trim());
1888
- writeTaskEntry(filePath, parsed, task);
1963
+ writeTaskEntry(root, filePath, parsed, task);
1889
1964
  console.log(`Commented ${task.id}`);
1890
1965
  });
1891
1966
  }
@@ -2137,7 +2212,6 @@ import {
2137
2212
  parseTaskFile as parseTaskFile7,
2138
2213
  writeYamlFile as writeYamlFile3,
2139
2214
  stringifyFrontmatter as stringifyFrontmatter4,
2140
- validateStructural as validateStructural5,
2141
2215
  writeTask as writeTask6
2142
2216
  } from "@kitsy/coop-core";
2143
2217
  import { create_provider_idea_decomposer, decompose_idea_to_tasks } from "@kitsy/coop-ai";
@@ -2166,7 +2240,7 @@ function renderSelect(question, choices, selected) {
2166
2240
  process2.stdout.write(` ${prefix} ${choice.label}${hint}
2167
2241
  `);
2168
2242
  }
2169
- process2.stdout.write("\nUse \u2191/\u2193 to choose, Enter to confirm.\n");
2243
+ process2.stdout.write("\nUse Up/Down arrows to choose, Enter to confirm.\n");
2170
2244
  }
2171
2245
  function moveCursorUp(lines) {
2172
2246
  if (lines <= 0) return;
@@ -2182,7 +2256,9 @@ async function select(question, choices, defaultIndex = 0) {
2182
2256
  }
2183
2257
  readline.emitKeypressEvents(process2.stdin);
2184
2258
  const previousRawMode = process2.stdin.isRaw;
2259
+ const wasPaused = typeof process2.stdin.isPaused === "function" ? process2.stdin.isPaused() : false;
2185
2260
  process2.stdin.setRawMode(true);
2261
+ process2.stdin.resume();
2186
2262
  let selected = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
2187
2263
  const renderedLines = choices.length + 2;
2188
2264
  renderSelect(question, choices, selected);
@@ -2190,6 +2266,9 @@ async function select(question, choices, defaultIndex = 0) {
2190
2266
  const cleanup = () => {
2191
2267
  process2.stdin.off("keypress", onKeypress);
2192
2268
  process2.stdin.setRawMode(previousRawMode ?? false);
2269
+ if (wasPaused) {
2270
+ process2.stdin.pause();
2271
+ }
2193
2272
  process2.stdout.write("\n");
2194
2273
  };
2195
2274
  const rerender = () => {
@@ -2631,7 +2710,7 @@ function plusDaysIso(days) {
2631
2710
  date.setUTCDate(date.getUTCDate() + days);
2632
2711
  return date.toISOString().slice(0, 10);
2633
2712
  }
2634
- function unique(values) {
2713
+ function unique2(values) {
2635
2714
  return Array.from(new Set(values));
2636
2715
  }
2637
2716
  function resolveIdeaFile2(root, idOrAlias) {
@@ -2639,7 +2718,7 @@ function resolveIdeaFile2(root, idOrAlias) {
2639
2718
  return path8.join(root, ...target.file.split("/"));
2640
2719
  }
2641
2720
  function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
2642
- const next = unique([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
2721
+ const next = unique2([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
2643
2722
  const nextRaw = {
2644
2723
  ...raw,
2645
2724
  linked_tasks: next
@@ -2654,10 +2733,10 @@ function makeTaskDraft(input2) {
2654
2733
  track: input2.track,
2655
2734
  priority: input2.priority,
2656
2735
  body: input2.body,
2657
- acceptance: unique(input2.acceptance ?? []),
2658
- testsRequired: unique(input2.testsRequired ?? []),
2659
- authorityRefs: unique(input2.authorityRefs ?? []),
2660
- derivedRefs: unique(input2.derivedRefs ?? [])
2736
+ acceptance: unique2(input2.acceptance ?? []),
2737
+ testsRequired: unique2(input2.testsRequired ?? []),
2738
+ authorityRefs: unique2(input2.authorityRefs ?? []),
2739
+ derivedRefs: unique2(input2.derivedRefs ?? [])
2661
2740
  };
2662
2741
  }
2663
2742
  function registerCreateCommand(program) {
@@ -2691,10 +2770,10 @@ function registerCreateCommand(program) {
2691
2770
  const priority = options.priority?.trim() || (interactive ? await ask("Priority", "p2") : "p2");
2692
2771
  const delivery = options.delivery?.trim() || (interactive ? await ask("Delivery (optional)", "") : "");
2693
2772
  const body = options.body ?? (interactive ? await ask("Task body (optional)", "") : "");
2694
- const acceptance = options.acceptance && options.acceptance.length > 0 ? unique(options.acceptance) : interactive ? parseCsv(await ask("Acceptance criteria (comma-separated, optional)", "")) : [];
2695
- const testsRequired = options.testsRequired && options.testsRequired.length > 0 ? unique(options.testsRequired) : interactive ? parseCsv(await ask("Tests required (comma-separated, optional)", "")) : [];
2696
- const authorityRefs = options.authorityRef && options.authorityRef.length > 0 ? unique(options.authorityRef) : interactive ? parseCsv(await ask("Authority refs (comma-separated, optional)", "")) : [];
2697
- const derivedRefs = options.derivedRef && options.derivedRef.length > 0 ? unique(options.derivedRef) : interactive ? parseCsv(await ask("Derived refs (comma-separated, optional)", "")) : [];
2773
+ const acceptance = options.acceptance && options.acceptance.length > 0 ? unique2(options.acceptance) : interactive ? parseCsv(await ask("Acceptance criteria (comma-separated, optional)", "")) : [];
2774
+ const testsRequired = options.testsRequired && options.testsRequired.length > 0 ? unique2(options.testsRequired) : interactive ? parseCsv(await ask("Tests required (comma-separated, optional)", "")) : [];
2775
+ const authorityRefs = options.authorityRef && options.authorityRef.length > 0 ? unique2(options.authorityRef) : interactive ? parseCsv(await ask("Authority refs (comma-separated, optional)", "")) : [];
2776
+ const derivedRefs = options.derivedRef && options.derivedRef.length > 0 ? unique2(options.derivedRef) : interactive ? parseCsv(await ask("Derived refs (comma-separated, optional)", "")) : [];
2698
2777
  const date = todayIsoDate();
2699
2778
  const drafts = [];
2700
2779
  let sourceIdeaPath = null;
@@ -2810,13 +2889,8 @@ function registerCreateCommand(program) {
2810
2889
  } : void 0
2811
2890
  };
2812
2891
  const filePath = path8.join(coop, "tasks", `${id}.md`);
2813
- const structuralIssues = validateStructural5(task, { filePath });
2814
- if (structuralIssues.length > 0) {
2815
- const message = structuralIssues.map((issue) => `- ${issue.message}`).join("\n");
2816
- throw new Error(`Task failed structural validation:
2817
- ${message}`);
2818
- }
2819
- writeTask6(task, {
2892
+ const normalizedTask = validateTaskForWrite(root, task, filePath);
2893
+ writeTask6(normalizedTask, {
2820
2894
  body: draft.body,
2821
2895
  filePath
2822
2896
  });
@@ -2892,7 +2966,7 @@ ${message}`);
2892
2966
  const interactive = Boolean(options.interactive);
2893
2967
  const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
2894
2968
  if (!name) throw new Error("Track name is required.");
2895
- const capacityProfiles = unique(
2969
+ const capacityProfiles = unique2(
2896
2970
  options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
2897
2971
  );
2898
2972
  const maxWipInput = options.maxWip?.trim() || (interactive ? await ask("Max concurrent tasks", "6") : "6");
@@ -2900,7 +2974,7 @@ ${message}`);
2900
2974
  if (typeof maxWip !== "number" || maxWip <= 0 || !Number.isInteger(maxWip)) {
2901
2975
  throw new Error("max-wip must be a positive integer.");
2902
2976
  }
2903
- const allowed = unique(
2977
+ const allowed = unique2(
2904
2978
  options.allowedTypes ? parseCsv(options.allowedTypes).map((entry) => entry.toLowerCase()) : interactive ? parseCsv(await ask("Allowed types (comma-separated)", "feature,bug,chore,spike")).map(
2905
2979
  (entry) => entry.toLowerCase()
2906
2980
  ) : ["feature", "bug", "chore", "spike"]
@@ -2982,7 +3056,7 @@ ${message}`);
2982
3056
  options.budgetCost?.trim() || (interactive ? await ask("Budget cost USD (optional)", "") : void 0),
2983
3057
  "budget-cost"
2984
3058
  );
2985
- const capacityProfiles = unique(
3059
+ const capacityProfiles = unique2(
2986
3060
  options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
2987
3061
  );
2988
3062
  const tasks = listTaskFiles(root).map((filePath2) => {
@@ -2995,12 +3069,12 @@ ${message}`);
2995
3069
  console.log(`- ${task.id}: ${task.title}`);
2996
3070
  }
2997
3071
  }
2998
- const scopeInclude = unique(
3072
+ const scopeInclude = unique2(
2999
3073
  options.scope ? parseCsv(options.scope).map((value) => value.toUpperCase()) : interactive ? parseCsv(await ask("Scope include task IDs (comma-separated)", tasks.map((task) => task.id).join(","))).map(
3000
3074
  (value) => value.toUpperCase()
3001
3075
  ) : []
3002
3076
  );
3003
- const scopeExclude = unique(
3077
+ const scopeExclude = unique2(
3004
3078
  options.exclude ? parseCsv(options.exclude).map((value) => value.toUpperCase()) : interactive ? parseCsv(await ask("Scope exclude task IDs (comma-separated)", "")).map((value) => value.toUpperCase()) : []
3005
3079
  );
3006
3080
  const knownTaskIds = new Set(tasks.map((task) => task.id));
@@ -3336,6 +3410,7 @@ var catalog = {
3336
3410
  selection_rules: [
3337
3411
  "Use `coop project show` first to confirm the active workspace and project.",
3338
3412
  "Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
3413
+ "Before assigning `track` or `delivery` values to tasks, inspect or create named entities with `coop list tracks`, `coop list deliveries`, `coop create track`, and `coop create delivery`.",
3339
3414
  "Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
3340
3415
  "Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
3341
3416
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
@@ -3393,6 +3468,8 @@ var catalog = {
3393
3468
  { usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
3394
3469
  { usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
3395
3470
  { usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
3471
+ { usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
3472
+ { usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
3396
3473
  { usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
3397
3474
  { usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
3398
3475
  { usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
@@ -3407,6 +3484,7 @@ var catalog = {
3407
3484
  { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
3408
3485
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3409
3486
  { usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
3487
+ { usage: "coop create track --id mvp --name MVP", purpose: "Create a named track before tasks refer to it." },
3410
3488
  {
3411
3489
  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',
3412
3490
  purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
@@ -3450,6 +3528,8 @@ var catalog = {
3450
3528
  description: "Read backlog state, task details, and planning output.",
3451
3529
  commands: [
3452
3530
  { usage: "coop list tasks --status todo", purpose: "List tasks with filters." },
3531
+ { usage: "coop list tracks", purpose: "List valid named tracks." },
3532
+ { usage: "coop list deliveries", purpose: "List valid named deliveries." },
3453
3533
  { 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
3534
  { usage: "coop list tasks --mine", purpose: "List tasks assigned to the current default COOP author." },
3455
3535
  { usage: 'coop search "auth and login form"', purpose: "Run deterministic non-AI search across tasks, ideas, and deliveries." },
@@ -3834,11 +3914,11 @@ function resolveHelpTopic(options) {
3834
3914
  if (options.naming) {
3835
3915
  requestedTopics.push("naming");
3836
3916
  }
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(", ")}.`);
3917
+ const unique4 = [...new Set(requestedTopics)];
3918
+ if (unique4.length > 1) {
3919
+ throw new Error(`Specify only one focused help-ai topic at a time. Received: ${unique4.join(", ")}.`);
3840
3920
  }
3841
- const topic = unique3[0];
3921
+ const topic = unique4[0];
3842
3922
  if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
3843
3923
  throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
3844
3924
  }
@@ -3904,7 +3984,7 @@ import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
3904
3984
  import fs8 from "fs";
3905
3985
  import path11 from "path";
3906
3986
  import { spawnSync as spawnSync3 } from "child_process";
3907
- import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural6 } from "@kitsy/coop-core";
3987
+ import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural5 } from "@kitsy/coop-core";
3908
3988
  var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
3909
3989
  var HOOK_BLOCK_END = "# COOP_PRE_COMMIT_END";
3910
3990
  function runGit(repoRoot, args, allowFailure = false) {
@@ -3988,7 +4068,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
3988
4068
  errors.push(`[COOP] ${message}`);
3989
4069
  continue;
3990
4070
  }
3991
- const issues = validateStructural6(task, { filePath: absolutePath });
4071
+ const issues = validateStructural5(task, { filePath: absolutePath });
3992
4072
  for (const issue of issues) {
3993
4073
  errors.push(`[COOP] ${relativePath}: ${issue.message}`);
3994
4074
  }
@@ -4643,9 +4723,48 @@ import {
4643
4723
  parseDeliveryFile as parseDeliveryFile2,
4644
4724
  parseIdeaFile as parseIdeaFile4,
4645
4725
  parseTaskFile as parseTaskFile10,
4726
+ parseYamlFile as parseYamlFile3,
4646
4727
  schedule_next as schedule_next3
4647
4728
  } from "@kitsy/coop-core";
4648
4729
  import chalk2 from "chalk";
4730
+ var TASK_COLUMN_WIDTHS = {
4731
+ id: 24,
4732
+ title: 30,
4733
+ status: 12,
4734
+ priority: 4,
4735
+ assignee: 16,
4736
+ track: 16,
4737
+ delivery: 16,
4738
+ score: 6,
4739
+ file: 30
4740
+ };
4741
+ var IDEA_COLUMN_WIDTHS = {
4742
+ id: 24,
4743
+ title: 30,
4744
+ status: 12,
4745
+ file: 30
4746
+ };
4747
+ function truncateCell(value, maxWidth) {
4748
+ if (maxWidth <= 0 || value.length <= maxWidth) {
4749
+ return value;
4750
+ }
4751
+ if (maxWidth <= 3) {
4752
+ return value.slice(0, maxWidth);
4753
+ }
4754
+ return `${value.slice(0, maxWidth - 3)}...`;
4755
+ }
4756
+ function truncateMiddleCell(value, maxWidth) {
4757
+ if (maxWidth <= 0 || value.length <= maxWidth) {
4758
+ return value;
4759
+ }
4760
+ if (maxWidth <= 3) {
4761
+ return value.slice(0, maxWidth);
4762
+ }
4763
+ const available = maxWidth - 3;
4764
+ const head = Math.ceil(available / 2);
4765
+ const tail = Math.floor(available / 2);
4766
+ return `${value.slice(0, head)}...${value.slice(value.length - tail)}`;
4767
+ }
4649
4768
  function statusColor(status) {
4650
4769
  switch (status) {
4651
4770
  case "done":
@@ -4884,8 +5003,16 @@ function listTasks(options) {
4884
5003
  ensureCoopInitialized(root);
4885
5004
  const context = readWorkingContext(root, resolveCoopHome());
4886
5005
  const graph = load_graph6(coopDir(root));
4887
- const resolvedTrack = resolveContextValueWithSource(options.track, context.track);
4888
- const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
5006
+ const rawResolvedTrack = resolveContextValueWithSource(options.track, context.track);
5007
+ const rawResolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
5008
+ const resolvedTrack = {
5009
+ ...rawResolvedTrack,
5010
+ value: rawResolvedTrack.value ? resolveExistingTrackId(root, rawResolvedTrack.value) ?? rawResolvedTrack.value : void 0
5011
+ };
5012
+ const resolvedDelivery = {
5013
+ ...rawResolvedDelivery,
5014
+ value: rawResolvedDelivery.value ? resolveExistingDeliveryId(root, rawResolvedDelivery.value) ?? rawResolvedDelivery.value : void 0
5015
+ };
4889
5016
  const resolvedVersion = resolveContextValueWithSource(options.version, context.version);
4890
5017
  const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
4891
5018
  const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
@@ -4942,27 +5069,30 @@ function listTasks(options) {
4942
5069
  columns.map(taskColumnHeader),
4943
5070
  sorted.map(
4944
5071
  (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
- }
5072
+ const rawValue = (() => {
5073
+ switch (column) {
5074
+ case "title":
5075
+ return entry.title;
5076
+ case "status":
5077
+ return statusColor(entry.status);
5078
+ case "priority":
5079
+ return entry.priority;
5080
+ case "assignee":
5081
+ return entry.assignee;
5082
+ case "track":
5083
+ return entry.track;
5084
+ case "delivery":
5085
+ return entry.delivery;
5086
+ case "score":
5087
+ return scoreMap.has(entry.id) ? scoreMap.get(entry.id).toFixed(1) : "-";
5088
+ case "file":
5089
+ return path15.relative(root, entry.filePath);
5090
+ case "id":
5091
+ default:
5092
+ return entry.id;
5093
+ }
5094
+ })();
5095
+ return column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
4966
5096
  })
4967
5097
  )
4968
5098
  )
@@ -5012,17 +5142,8 @@ function listIdeas(options) {
5012
5142
  columns.map(ideaColumnHeader),
5013
5143
  sorted.map(
5014
5144
  (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
- }
5145
+ const rawValue = column === "title" ? entry.title : column === "status" ? statusColor(entry.status) : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
5146
+ return column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
5026
5147
  })
5027
5148
  )
5028
5149
  )
@@ -5033,11 +5154,11 @@ Total ideas: ${sorted.length}`);
5033
5154
  function listDeliveries() {
5034
5155
  const root = resolveRepoRoot();
5035
5156
  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)
5157
+ truncateCell(delivery.id, 24),
5158
+ truncateCell(delivery.name, 30),
5159
+ truncateCell(statusColor(delivery.status), 12),
5160
+ truncateCell(delivery.target_date ?? "-", 16),
5161
+ truncateMiddleCell(path15.relative(root, filePath), 30)
5041
5162
  ]);
5042
5163
  if (rows.length === 0) {
5043
5164
  console.log("No deliveries found.");
@@ -5045,6 +5166,21 @@ function listDeliveries() {
5045
5166
  }
5046
5167
  console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
5047
5168
  }
5169
+ function listTracks() {
5170
+ const root = resolveRepoRoot();
5171
+ const rows = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile3(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id)).map(({ track, filePath }) => [
5172
+ truncateCell(track.id, 24),
5173
+ truncateCell(track.name, 30),
5174
+ truncateCell((track.capacity_profiles ?? []).join(", ") || "-", 24),
5175
+ truncateCell(String(track.constraints?.max_concurrent_tasks ?? "-"), 8),
5176
+ truncateMiddleCell(path15.relative(root, filePath), 30)
5177
+ ]);
5178
+ if (rows.length === 0) {
5179
+ console.log("No tracks found.");
5180
+ return;
5181
+ }
5182
+ console.log(formatTable(["ID", "Name", "Profiles", "Max WIP", "File"], rows));
5183
+ }
5048
5184
  function registerListCommand(program) {
5049
5185
  const list = program.command("list").description("List COOP entities");
5050
5186
  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) => {
@@ -5056,7 +5192,10 @@ function registerListCommand(program) {
5056
5192
  list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
5057
5193
  listAliasRows(pattern);
5058
5194
  });
5059
- list.command("deliveries").description("List deliveries").action(() => {
5195
+ list.command("tracks").alias("track").description("List tracks").action(() => {
5196
+ listTracks();
5197
+ });
5198
+ list.command("deliveries").alias("delivery").description("List deliveries").action(() => {
5060
5199
  listDeliveries();
5061
5200
  });
5062
5201
  }
@@ -5191,7 +5330,7 @@ function registerLogTimeCommand(program) {
5191
5330
  hours,
5192
5331
  options.note?.trim() || void 0
5193
5332
  );
5194
- writeTaskEntry(filePath, parsed, task);
5333
+ writeTaskEntry(root, filePath, parsed, task);
5195
5334
  console.log(`Logged ${hours}h ${options.kind} on ${task.id}`);
5196
5335
  });
5197
5336
  }
@@ -5201,7 +5340,7 @@ import fs12 from "fs";
5201
5340
  import path17 from "path";
5202
5341
  import { createInterface } from "readline/promises";
5203
5342
  import { stdin as input, stdout as output } from "process";
5204
- import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
5343
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile4, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
5205
5344
  var COOP_IGNORE_TEMPLATE2 = `.index/
5206
5345
  logs/
5207
5346
  tmp/
@@ -5368,7 +5507,7 @@ async function migrateWorkspaceLayout(root, options) {
5368
5507
  }
5369
5508
  const movedConfigPath = path17.join(projectRoot, "config.yml");
5370
5509
  if (fs12.existsSync(movedConfigPath)) {
5371
- const movedConfig = parseYamlFile2(movedConfigPath);
5510
+ const movedConfig = parseYamlFile4(movedConfigPath);
5372
5511
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
5373
5512
  nextProject.name = identity.projectName;
5374
5513
  nextProject.id = projectId;
@@ -5763,7 +5902,7 @@ function registerPromoteCommand(program) {
5763
5902
  track: resolvedTrack.value,
5764
5903
  version: resolvedVersion.value
5765
5904
  });
5766
- writeTaskEntry(filePath, parsed, task);
5905
+ writeTaskEntry(root, filePath, parsed, task);
5767
5906
  console.log(`Promoted ${task.id}`);
5768
5907
  });
5769
5908
  }
@@ -7012,7 +7151,7 @@ function maybePromote(root, taskId, options) {
7012
7151
  track: options.track ?? context.track,
7013
7152
  version: options.version ?? context.version
7014
7153
  });
7015
- writeTaskEntry(filePath, parsed, promoted);
7154
+ writeTaskEntry(root, filePath, parsed, promoted);
7016
7155
  console.log(`Promoted ${promoted.id}`);
7017
7156
  }
7018
7157
  function printResolvedSelectionContext(root, options) {
@@ -7224,7 +7363,7 @@ import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as
7224
7363
  function collect(value, previous = []) {
7225
7364
  return [...previous, value];
7226
7365
  }
7227
- function unique2(items) {
7366
+ function unique3(items) {
7228
7367
  return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
7229
7368
  }
7230
7369
  function removeValues(source, values) {
@@ -7234,7 +7373,7 @@ function removeValues(source, values) {
7234
7373
  return next.length > 0 ? next : void 0;
7235
7374
  }
7236
7375
  function addValues(source, values) {
7237
- const next = unique2([...source ?? [], ...values ?? []]);
7376
+ const next = unique3([...source ?? [], ...values ?? []]);
7238
7377
  return next.length > 0 ? next : void 0;
7239
7378
  }
7240
7379
  function loadBody(options) {
@@ -7316,13 +7455,13 @@ function updateTask(id, options) {
7316
7455
  tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
7317
7456
  };
7318
7457
  next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
7319
- validateTaskForWrite(next, filePath);
7458
+ next = validateTaskForWrite(root, next, filePath);
7320
7459
  const nextBody = loadBody(options) ?? parsed.body;
7321
7460
  if (options.dryRun) {
7322
7461
  console.log(renderTaskPreview(next, nextBody).trimEnd());
7323
7462
  return;
7324
7463
  }
7325
- writeTaskEntry(filePath, parsed, next, nextBody);
7464
+ writeTaskEntry(root, filePath, parsed, next, nextBody);
7326
7465
  console.log(`Updated ${next.id}`);
7327
7466
  }
7328
7467
  function updateIdea(id, options) {
@@ -7392,11 +7531,19 @@ function registerUseCommand(program) {
7392
7531
  });
7393
7532
  use.command("track").description("Set the default working track").argument("<id>", "Track id").action((id) => {
7394
7533
  const root = resolveRepoRoot();
7395
- printContext(updateWorkingContext(root, resolveCoopHome(), { track: id }));
7534
+ const trackId = resolveExistingTrackId(root, id);
7535
+ if (!trackId) {
7536
+ throw new Error(`Unknown track '${id}'. Create it first with \`coop create track --id ${id} --name "${id}"\` or use \`unassigned\`.`);
7537
+ }
7538
+ printContext(updateWorkingContext(root, resolveCoopHome(), { track: trackId }));
7396
7539
  });
7397
7540
  use.command("delivery").description("Set the default working delivery").argument("<id>", "Delivery id").action((id) => {
7398
7541
  const root = resolveRepoRoot();
7399
- printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: id }));
7542
+ const deliveryId = resolveExistingDeliveryId(root, id);
7543
+ if (!deliveryId) {
7544
+ throw new Error(`Unknown delivery '${id}'. Create it first with \`coop create delivery --id ${id} --name "${id}"\`.`);
7545
+ }
7546
+ printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: deliveryId }));
7400
7547
  });
7401
7548
  use.command("version").description("Set the default working version").argument("<id>", "Version label").action((id) => {
7402
7549
  const root = resolveRepoRoot();
@@ -7887,6 +8034,7 @@ function renderBasicHelp() {
7887
8034
  "Day-to-day commands:",
7888
8035
  "- `coop current`: show working context, active work, and the next ready task",
7889
8036
  "- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
8037
+ "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
7890
8038
  "- `coop next task` or `coop pick task`: choose work from COOP",
7891
8039
  "- `coop show <id>`: inspect a task, idea, or delivery",
7892
8040
  "- `coop list tasks --track <id>`: browse scoped work",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kitsy/coop",
3
3
  "description": "COOP command-line interface.",
4
- "version": "2.2.1",
4
+ "version": "2.2.2",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -39,9 +39,9 @@
39
39
  "chalk": "^5.6.2",
40
40
  "commander": "^14.0.0",
41
41
  "octokit": "^5.0.5",
42
- "@kitsy/coop-ai": "2.2.1",
43
- "@kitsy/coop-ui": "^2.2.1",
44
- "@kitsy/coop-core": "2.2.1"
42
+ "@kitsy/coop-core": "2.2.2",
43
+ "@kitsy/coop-ai": "2.2.2",
44
+ "@kitsy/coop-ui": "^2.2.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24.12.0",