@kitsy/coop 2.2.1 → 2.2.3

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 +1039 -190
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -32,7 +32,31 @@ var SEQ_MARKER = "COOPSEQTOKEN";
32
32
  var DEFAULT_ID_NAMING_TEMPLATE = "<TYPE>-<TITLE16>-<SEQ>";
33
33
  var DEFAULT_ARTIFACTS_DIR = "docs";
34
34
  var DEFAULT_TITLE_TOKEN_LENGTH = 16;
35
+ var DEFAULT_NAMING_TEMPLATES = {
36
+ task: DEFAULT_ID_NAMING_TEMPLATE,
37
+ idea: DEFAULT_ID_NAMING_TEMPLATE,
38
+ track: "<NAME_SLUG>",
39
+ delivery: "<NAME_SLUG>",
40
+ run: "<TYPE>-<YYMMDD>-<RAND>"
41
+ };
35
42
  var SEMANTIC_WORD_MAX = 4;
43
+ var BUILT_IN_NAMING_TOKENS = /* @__PURE__ */ new Set([
44
+ "TYPE",
45
+ "ENTITY",
46
+ "TITLE",
47
+ "TITLE16",
48
+ "TITLE24",
49
+ "TRACK",
50
+ "STATUS",
51
+ "TASK_TYPE",
52
+ "PREFIX",
53
+ "USER",
54
+ "YYMMDD",
55
+ "RAND",
56
+ "SEQ",
57
+ "NAME",
58
+ "NAME_SLUG"
59
+ ]);
36
60
  var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
37
61
  "A",
38
62
  "AN",
@@ -60,8 +84,12 @@ function resolveCoopHome() {
60
84
  }
61
85
  function resolveRepoRoot(start = process.cwd()) {
62
86
  let current = path.resolve(start);
87
+ const configuredCoopHome = path.resolve(resolveCoopHome());
88
+ const defaultCoopHome = path.resolve(path.join(os.homedir(), ".coop"));
63
89
  while (true) {
64
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".coop"))) {
90
+ const coopDir2 = path.join(current, ".coop");
91
+ const isGlobalCoopHome = path.resolve(coopDir2) === configuredCoopHome || path.resolve(coopDir2) === defaultCoopHome;
92
+ if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(coopDir2) && !isGlobalCoopHome) {
65
93
  return current;
66
94
  }
67
95
  const parent = path.dirname(current);
@@ -126,6 +154,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
126
154
  taskPrefix: "PM",
127
155
  indexDataFormat: "yaml",
128
156
  idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
157
+ idNamingTemplates: { ...DEFAULT_NAMING_TEMPLATES },
158
+ idTokens: {},
129
159
  idSeqPadding: 0,
130
160
  artifactsDir: DEFAULT_ARTIFACTS_DIR,
131
161
  projectName: repoName || "COOP Workspace",
@@ -147,9 +177,10 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
147
177
  const indexRaw = typeof config.index === "object" && config.index !== null ? config.index : {};
148
178
  const indexDataRaw = indexRaw.data;
149
179
  const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
180
+ const idNamingTemplates = readNamingTemplates(config);
181
+ const idNamingTemplate = idNamingTemplates.task;
182
+ const idTokens = readNamingTokens(config);
150
183
  const idRaw = typeof config.id === "object" && config.id !== null ? config.id : {};
151
- const idNamingTemplateRaw = idRaw.naming;
152
- const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : DEFAULT_ID_NAMING_TEMPLATE;
153
184
  const idSeqPaddingRaw = idRaw.seq_padding;
154
185
  const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
155
186
  const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
@@ -159,6 +190,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
159
190
  taskPrefix,
160
191
  indexDataFormat,
161
192
  idNamingTemplate,
193
+ idNamingTemplates,
194
+ idTokens,
162
195
  idSeqPadding,
163
196
  artifactsDir,
164
197
  projectName: projectName || "COOP Workspace",
@@ -176,6 +209,59 @@ function sanitizeIdentityPart(value, fallback) {
176
209
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
177
210
  return normalized || fallback;
178
211
  }
212
+ function slugifyLower(value, fallback = "item") {
213
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
214
+ return normalized || fallback;
215
+ }
216
+ function normalizeNamingTokenName(value) {
217
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
218
+ if (!normalized) {
219
+ throw new Error("Naming token names must contain letters, numbers, or underscores.");
220
+ }
221
+ return normalized;
222
+ }
223
+ function normalizeNamingTokenValue(value) {
224
+ const normalized = value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
225
+ if (!normalized) {
226
+ throw new Error("Naming token values must contain letters or numbers.");
227
+ }
228
+ return normalized;
229
+ }
230
+ function readNamingTemplates(rawConfig) {
231
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
232
+ const namingRaw = idRaw.naming;
233
+ if (typeof namingRaw === "string" && namingRaw.trim().length > 0) {
234
+ return {
235
+ ...DEFAULT_NAMING_TEMPLATES,
236
+ task: namingRaw.trim(),
237
+ idea: namingRaw.trim()
238
+ };
239
+ }
240
+ const namingRecord = typeof namingRaw === "object" && namingRaw !== null ? namingRaw : {};
241
+ return {
242
+ task: typeof namingRecord.task === "string" && namingRecord.task.trim().length > 0 ? namingRecord.task.trim() : DEFAULT_NAMING_TEMPLATES.task,
243
+ idea: typeof namingRecord.idea === "string" && namingRecord.idea.trim().length > 0 ? namingRecord.idea.trim() : DEFAULT_NAMING_TEMPLATES.idea,
244
+ track: typeof namingRecord.track === "string" && namingRecord.track.trim().length > 0 ? namingRecord.track.trim() : DEFAULT_NAMING_TEMPLATES.track,
245
+ delivery: typeof namingRecord.delivery === "string" && namingRecord.delivery.trim().length > 0 ? namingRecord.delivery.trim() : DEFAULT_NAMING_TEMPLATES.delivery,
246
+ run: typeof namingRecord.run === "string" && namingRecord.run.trim().length > 0 ? namingRecord.run.trim() : DEFAULT_NAMING_TEMPLATES.run
247
+ };
248
+ }
249
+ function readNamingTokens(rawConfig) {
250
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
251
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? idRaw.tokens : {};
252
+ const tokens = {};
253
+ for (const [rawName, rawValue] of Object.entries(tokensRaw)) {
254
+ const name = normalizeNamingTokenName(rawName);
255
+ const valuesRecord = typeof rawValue === "object" && rawValue !== null ? rawValue : {};
256
+ const values = Array.isArray(valuesRecord.values) ? Array.from(
257
+ new Set(
258
+ valuesRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
259
+ )
260
+ ).sort((a, b) => a.localeCompare(b)) : [];
261
+ tokens[name] = { values };
262
+ }
263
+ return tokens;
264
+ }
179
265
  function repoDisplayName(root) {
180
266
  const base = path.basename(path.resolve(root)).trim();
181
267
  return base || "COOP Workspace";
@@ -278,6 +364,19 @@ function sanitizeTemplateValue(input2, fallback = "X") {
278
364
  const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
279
365
  return normalized || fallback;
280
366
  }
367
+ function normalizeEntityIdValue(entityType, input2, fallback = "COOP-ID") {
368
+ if (entityType === "track" || entityType === "delivery") {
369
+ return slugifyLower(input2, slugifyLower(fallback, "item"));
370
+ }
371
+ return sanitizeTemplateValue(input2, fallback);
372
+ }
373
+ function extractTemplateTokens(template) {
374
+ return Array.from(
375
+ new Set(
376
+ Array.from(template.matchAll(/<([^>]+)>/g)).map((match) => match[1]?.trim().toUpperCase()).filter((token) => Boolean(token))
377
+ )
378
+ );
379
+ }
281
380
  function sanitizeSemanticWord(input2) {
282
381
  return input2.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
283
382
  }
@@ -332,8 +431,8 @@ function namingTokenExamples(title = "Natural-language COOP command recommender"
332
431
  };
333
432
  }
334
433
  function sequenceForPattern(existingIds, prefix, suffix) {
335
- const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
336
- const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
434
+ const escapedPrefix = prefix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
435
+ const escapedSuffix = suffix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
337
436
  const regex = new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`);
338
437
  let max = 0;
339
438
  for (const id of existingIds) {
@@ -359,6 +458,7 @@ function buildIdContext(root, config, context) {
359
458
  const track = context.track?.trim() || "";
360
459
  const status = context.status?.trim() || "";
361
460
  const title = context.title?.trim() || "";
461
+ const name = context.name?.trim() || context.title?.trim() || "";
362
462
  const prefix = context.prefix?.trim() || (context.entityType === "idea" ? config.ideaPrefix : config.taskPrefix);
363
463
  const actor = inferActor(root);
364
464
  const map = {
@@ -373,13 +473,26 @@ function buildIdContext(root, config, context) {
373
473
  STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
374
474
  TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
375
475
  PREFIX: sanitizeTemplateValue(prefix, "COOP"),
476
+ NAME: name || "item",
477
+ NAME_SLUG: slugifyLower(name || "item"),
376
478
  RAND: randomToken()
377
479
  };
378
480
  for (const [key, value] of Object.entries(fields)) {
379
481
  if (!value || !value.trim()) continue;
380
- const upper = key.toUpperCase();
482
+ const tokenName = normalizeNamingTokenName(key);
483
+ const upper = tokenName.toUpperCase();
381
484
  if (Object.prototype.hasOwnProperty.call(map, upper)) continue;
382
- map[upper] = sanitizeTemplateValue(value);
485
+ const tokenConfig = config.idTokens[tokenName];
486
+ if (!tokenConfig) {
487
+ continue;
488
+ }
489
+ const normalizedValue = normalizeNamingTokenValue(value);
490
+ if (tokenConfig.values.length > 0 && !tokenConfig.values.includes(normalizedValue)) {
491
+ throw new Error(
492
+ `Invalid value '${value}' for naming token '${tokenName}'. Allowed values: ${tokenConfig.values.join(", ")}.`
493
+ );
494
+ }
495
+ map[upper] = normalizedValue;
383
496
  }
384
497
  return map;
385
498
  }
@@ -398,52 +511,137 @@ function replaceTemplateToken(token, contextMap) {
398
511
  if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
399
512
  if (upper === "STATUS") return contextMap.STATUS;
400
513
  if (upper === "PREFIX") return contextMap.PREFIX;
514
+ if (upper === "NAME") return contextMap.NAME;
515
+ if (upper === "NAME_SLUG") return contextMap.NAME_SLUG;
401
516
  const dynamic = contextMap[upper];
402
517
  if (dynamic) return dynamic;
403
518
  return sanitizeTemplateValue(upper);
404
519
  }
405
- function renderNamingTemplate(template, contextMap) {
520
+ function renderNamingTemplate(template, contextMap, entityType) {
406
521
  const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
407
522
  const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
408
- return sanitizeTemplateValue(replaced, "COOP-ID");
523
+ return normalizeEntityIdValue(entityType, replaced, "COOP-ID");
409
524
  }
410
525
  function defaultCoopAuthor(root) {
411
526
  const actor = inferActor(root);
412
527
  return actor.trim() || "unknown";
413
528
  }
529
+ function normalizeEntityId(entityType, value) {
530
+ const trimmed = value.trim();
531
+ if (!trimmed) {
532
+ throw new Error(`Invalid ${entityType} id. Provide a non-empty value.`);
533
+ }
534
+ return normalizeEntityIdValue(entityType, trimmed, entityType);
535
+ }
536
+ function namingTemplatesForRoot(root) {
537
+ return readCoopConfig(root).idNamingTemplates;
538
+ }
539
+ function namingTokensForRoot(root) {
540
+ return readCoopConfig(root).idTokens;
541
+ }
542
+ function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
543
+ const config = readCoopConfig(root);
544
+ const digest = crypto.createHash("sha256").update(`${config.projectId}:${entityType}:${primaryId}`).digest("hex").toLowerCase();
545
+ const existing = new Set(existingShortIds.map((value) => value.toLowerCase()));
546
+ for (let width = 12; width <= digest.length; width += 2) {
547
+ const candidate = digest.slice(0, width);
548
+ if (!existing.has(candidate)) {
549
+ return candidate;
550
+ }
551
+ }
552
+ throw new Error(`Unable to generate a unique short id for ${entityType} '${primaryId}'.`);
553
+ }
554
+ function extractDynamicTokenFlags(commandPath, knownValueOptions, knownBooleanOptions = [], argv = process.argv.slice(2)) {
555
+ const start = argv.findIndex((_, index) => commandPath.every((segment, offset) => argv[index + offset] === segment));
556
+ if (start < 0) {
557
+ return {};
558
+ }
559
+ const result = {};
560
+ const valueOptions = new Set(knownValueOptions);
561
+ const booleanOptions = new Set(knownBooleanOptions);
562
+ for (let index = start + commandPath.length; index < argv.length; index += 1) {
563
+ const token = argv[index] ?? "";
564
+ if (!token.startsWith("--")) {
565
+ continue;
566
+ }
567
+ const [rawName, inlineValue] = token.slice(2).split("=", 2);
568
+ const name = rawName.trim();
569
+ if (!name) {
570
+ continue;
571
+ }
572
+ if (valueOptions.has(name)) {
573
+ if (inlineValue === void 0) {
574
+ index += 1;
575
+ }
576
+ continue;
577
+ }
578
+ if (booleanOptions.has(name) || name.startsWith("no-")) {
579
+ continue;
580
+ }
581
+ const value = inlineValue ?? argv[index + 1];
582
+ if (!value || value.startsWith("-")) {
583
+ throw new Error(`Dynamic naming flag '--${name}' requires a value.`);
584
+ }
585
+ result[name] = value;
586
+ if (inlineValue === void 0) {
587
+ index += 1;
588
+ }
589
+ }
590
+ return result;
591
+ }
414
592
  function previewNamingTemplate(template, context, root = process.cwd()) {
415
593
  const config = readCoopConfig(root);
594
+ const usedTemplate = template.trim().length > 0 ? template : config.idNamingTemplates[context.entityType];
595
+ const referencedTokens = extractTemplateTokens(usedTemplate);
596
+ for (const token of referencedTokens) {
597
+ if (!BUILT_IN_NAMING_TOKENS.has(token) && !config.idTokens[token.toLowerCase()]) {
598
+ throw new Error(`Naming template references unknown token <${token}>.`);
599
+ }
600
+ }
416
601
  const contextMap = buildIdContext(root, config, context);
417
- const rendered = renderNamingTemplate(template, {
602
+ const rendered = renderNamingTemplate(usedTemplate, {
418
603
  ...contextMap,
419
604
  RAND: "AB12CD34",
420
605
  YYMMDD: "260320",
421
606
  USER: "PKVSI"
422
- });
607
+ }, context.entityType);
423
608
  return rendered.replace(SEQ_MARKER, "1");
424
609
  }
425
610
  function generateConfiguredId(root, existingIds, context) {
426
611
  const config = readCoopConfig(root);
612
+ const template = config.idNamingTemplates[context.entityType];
613
+ const referencedTokens = extractTemplateTokens(template);
614
+ for (const token of referencedTokens) {
615
+ if (BUILT_IN_NAMING_TOKENS.has(token)) {
616
+ continue;
617
+ }
618
+ if (!config.idTokens[token.toLowerCase()]) {
619
+ throw new Error(`Naming template references unknown token <${token}>.`);
620
+ }
621
+ if (!context.fields || !Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === token.toLowerCase())) {
622
+ throw new Error(`Naming template requires token <${token}>. Pass --${token.toLowerCase()} <value>.`);
623
+ }
624
+ }
427
625
  const contextMap = buildIdContext(root, config, context);
428
626
  const existing = existingIds.map((id) => id.toUpperCase());
429
627
  for (let attempt = 0; attempt < 32; attempt += 1) {
430
- const rendered = renderNamingTemplate(config.idNamingTemplate, {
628
+ const rendered = renderNamingTemplate(template, {
431
629
  ...contextMap,
432
630
  RAND: randomToken()
433
- });
631
+ }, context.entityType);
434
632
  const seqMarker = SEQ_MARKER;
435
- const upperRendered = rendered.toUpperCase();
436
- if (!upperRendered.includes(seqMarker)) {
437
- if (!existing.includes(upperRendered)) {
438
- return upperRendered;
633
+ const normalizedRendered = context.entityType === "track" || context.entityType === "delivery" ? rendered : rendered.toUpperCase();
634
+ if (!normalizedRendered.includes(seqMarker)) {
635
+ if (!existing.includes(normalizedRendered.toUpperCase())) {
636
+ return normalizedRendered;
439
637
  }
440
638
  continue;
441
639
  }
442
- const [prefix, suffix] = upperRendered.split(seqMarker);
640
+ const [prefix, suffix] = normalizedRendered.split(seqMarker);
443
641
  const nextSeq = sequenceForPattern(existing, prefix ?? "", suffix ?? "");
444
642
  const seq = padSequence(nextSeq, config.idSeqPadding);
445
643
  const candidate = `${prefix ?? ""}${seq}${suffix ?? ""}`.replace(/-+/g, "-").replace(/^-+|-+$/g, "");
446
- if (!existing.includes(candidate)) {
644
+ if (!existing.includes(candidate.toUpperCase())) {
447
645
  return candidate;
448
646
  }
449
647
  }
@@ -551,32 +749,58 @@ function rebuildAliasIndex(root) {
551
749
  ensureCoopInitialized(root);
552
750
  const items = {};
553
751
  const aliases = {};
752
+ const shortIds = {};
554
753
  const ids = /* @__PURE__ */ new Set();
555
754
  const taskFiles = listTaskFiles(root);
755
+ const usedTaskShortIds = taskFiles.map((filePath) => parseTaskFile2(filePath)).map((parsed) => parsed.task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
556
756
  for (const filePath of taskFiles) {
557
757
  const parsed = parseTaskFile2(filePath);
558
758
  const id = parsed.task.id.toUpperCase();
559
759
  const source = path2.relative(root, filePath);
760
+ const shortId = parsed.task.short_id?.trim() ? parsed.task.short_id.trim().toLowerCase() : generateStableShortId(root, "task", parsed.task.id, usedTaskShortIds);
761
+ if (!parsed.task.short_id?.trim()) {
762
+ usedTaskShortIds.push(shortId);
763
+ const nextRaw = { ...parsed.raw, short_id: shortId };
764
+ const nextTask = { ...parsed.task, short_id: shortId };
765
+ writeTask(nextTask, { body: parsed.body, raw: nextRaw, filePath });
766
+ }
560
767
  ids.add(id);
561
768
  items[id] = {
562
769
  type: "task",
563
770
  aliases: parseAliases(parsed.raw, source),
564
- file: toPosixPath(path2.relative(root, filePath))
771
+ file: toPosixPath(path2.relative(root, filePath)),
772
+ short_id: shortId
565
773
  };
566
774
  }
567
775
  const ideaFiles = listIdeaFiles(root);
776
+ const usedIdeaShortIds = ideaFiles.map((filePath) => parseIdeaFile(filePath)).map((parsed) => parsed.idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
568
777
  for (const filePath of ideaFiles) {
569
778
  const parsed = parseIdeaFile(filePath);
570
779
  const id = parsed.idea.id.toUpperCase();
571
780
  const source = path2.relative(root, filePath);
781
+ const shortId = parsed.idea.short_id?.trim() ? parsed.idea.short_id.trim().toLowerCase() : generateStableShortId(root, "idea", parsed.idea.id, usedIdeaShortIds);
782
+ if (!parsed.idea.short_id?.trim()) {
783
+ usedIdeaShortIds.push(shortId);
784
+ const nextRaw = { ...parsed.raw, short_id: shortId };
785
+ const output2 = stringifyFrontmatter(nextRaw, parsed.body);
786
+ fs2.writeFileSync(filePath, output2, "utf8");
787
+ }
572
788
  ids.add(id);
573
789
  items[id] = {
574
790
  type: "idea",
575
791
  aliases: parseAliases(parsed.raw, source),
576
- file: toPosixPath(path2.relative(root, filePath))
792
+ file: toPosixPath(path2.relative(root, filePath)),
793
+ short_id: shortId
577
794
  };
578
795
  }
579
796
  for (const [id, item] of Object.entries(items)) {
797
+ if (item.short_id) {
798
+ const existingShort = shortIds[item.short_id];
799
+ if (existingShort && existingShort.id !== id) {
800
+ throw new Error(`Short id '${item.short_id}' is already mapped to '${existingShort.id}'.`);
801
+ }
802
+ shortIds[item.short_id] = { id, type: item.type, file: item.file, short_id: item.short_id };
803
+ }
580
804
  for (const alias of item.aliases) {
581
805
  if (ids.has(alias)) {
582
806
  throw new Error(`Alias '${alias}' conflicts with existing item id '${alias}'.`);
@@ -592,6 +816,7 @@ function rebuildAliasIndex(root) {
592
816
  version: 1,
593
817
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
594
818
  aliases,
819
+ short_ids: shortIds,
595
820
  items
596
821
  };
597
822
  writeIndexFile(root, data);
@@ -604,7 +829,7 @@ function loadAliasIndex(root) {
604
829
  return rebuildAliasIndex(root);
605
830
  }
606
831
  const parsed = readIndexFile(root, aliasIndexPath);
607
- if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items) {
832
+ if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items || !parsed.short_ids) {
608
833
  return rebuildAliasIndex(root);
609
834
  }
610
835
  return parsed;
@@ -619,6 +844,27 @@ function resolveReference(root, idOrAlias, expectedType) {
619
844
  }
620
845
  return { id: idCandidate, type: item.type, file: item.file };
621
846
  }
847
+ const shortCandidate = idOrAlias.trim().toLowerCase();
848
+ const shortMatch = index.short_ids[shortCandidate];
849
+ if (shortMatch) {
850
+ if (expectedType && shortMatch.type !== expectedType) {
851
+ throw new Error(`'${idOrAlias}' resolves to ${shortMatch.type} '${shortMatch.id}', expected ${expectedType}.`);
852
+ }
853
+ return shortMatch;
854
+ }
855
+ if (shortCandidate.length >= 6) {
856
+ const prefixMatches = Object.entries(index.short_ids).filter(([shortId]) => shortId.startsWith(shortCandidate)).map(([, target]) => target);
857
+ if (prefixMatches.length === 1) {
858
+ const match2 = prefixMatches[0];
859
+ if (expectedType && match2.type !== expectedType) {
860
+ throw new Error(`'${idOrAlias}' resolves to ${match2.type} '${match2.id}', expected ${expectedType}.`);
861
+ }
862
+ return match2;
863
+ }
864
+ if (prefixMatches.length > 1) {
865
+ throw new Error(`Ambiguous short id '${idOrAlias}'. Candidates: ${prefixMatches.map((entry) => entry.id).join(", ")}.`);
866
+ }
867
+ }
622
868
  const alias = normalizeAlias(idOrAlias);
623
869
  const match = index.aliases[alias];
624
870
  if (!match) {
@@ -725,17 +971,28 @@ function removeAliases(root, idOrAlias, values) {
725
971
  }
726
972
 
727
973
  // src/utils/table.ts
974
+ var ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
975
+ function visibleLength(value) {
976
+ return value.replace(ANSI_PATTERN, "").length;
977
+ }
978
+ function padEndAnsi(value, width) {
979
+ const visible = visibleLength(value);
980
+ if (visible >= width) {
981
+ return value;
982
+ }
983
+ return `${value}${" ".repeat(width - visible)}`;
984
+ }
728
985
  function formatTable(headers, rows) {
729
986
  const table = [headers, ...rows];
730
987
  const widths = [];
731
988
  for (let col = 0; col < headers.length; col += 1) {
732
- let width = headers[col].length;
989
+ let width = visibleLength(headers[col] ?? "");
733
990
  for (const row of table) {
734
- width = Math.max(width, (row[col] ?? "").length);
991
+ width = Math.max(width, visibleLength(row[col] ?? ""));
735
992
  }
736
993
  widths.push(width + 2);
737
994
  }
738
- return table.map((row) => row.map((cell, index) => (cell ?? "").padEnd(widths[index])).join("").trimEnd()).join("\n");
995
+ return table.map((row) => row.map((cell, index) => padEndAnsi(cell ?? "", widths[index])).join("").trimEnd()).join("\n");
739
996
  }
740
997
 
741
998
  // src/commands/alias.ts
@@ -1391,14 +1648,148 @@ import fs3 from "fs";
1391
1648
  import path3 from "path";
1392
1649
  import {
1393
1650
  effective_priority,
1651
+ parseYamlFile as parseYamlFile2,
1394
1652
  parseDeliveryFile,
1395
1653
  parseIdeaFile as parseIdeaFile2,
1396
1654
  parseTaskFile as parseTaskFile4,
1397
1655
  stringifyFrontmatter as stringifyFrontmatter2,
1398
1656
  validateStructural as validateStructural2,
1399
1657
  validateSemantic,
1658
+ writeYamlFile as writeYamlFile3,
1400
1659
  writeTask as writeTask3
1401
1660
  } from "@kitsy/coop-core";
1661
+ function writeTrackFile(filePath, track) {
1662
+ writeYamlFile3(filePath, track);
1663
+ }
1664
+ function writeDeliveryFile(filePath, delivery, raw, body) {
1665
+ const payload = { ...raw, ...delivery };
1666
+ if (body.trim().length > 0) {
1667
+ fs3.writeFileSync(filePath, stringifyFrontmatter2(payload, body), "utf8");
1668
+ return;
1669
+ }
1670
+ writeYamlFile3(filePath, payload);
1671
+ }
1672
+ function loadTrackEntries(root) {
1673
+ const rawEntries = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile2(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id));
1674
+ const used = rawEntries.map((entry) => entry.track.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1675
+ let mutated = false;
1676
+ for (const entry of rawEntries) {
1677
+ if (entry.track.short_id?.trim()) {
1678
+ continue;
1679
+ }
1680
+ entry.track.short_id = generateStableShortId(root, "track", entry.track.id, used);
1681
+ used.push(entry.track.short_id);
1682
+ writeTrackFile(entry.filePath, entry.track);
1683
+ mutated = true;
1684
+ }
1685
+ return mutated ? rawEntries.map((entry) => ({ track: parseYamlFile2(entry.filePath), filePath: entry.filePath })) : rawEntries;
1686
+ }
1687
+ function loadDeliveryEntries(root) {
1688
+ const rawEntries = listDeliveryFiles(root).map((filePath) => {
1689
+ const parsed = parseDeliveryFile(filePath);
1690
+ return { delivery: parsed.delivery, filePath, body: parsed.body, raw: parsed.raw };
1691
+ }).sort((a, b) => a.delivery.id.localeCompare(b.delivery.id));
1692
+ const used = rawEntries.map((entry) => entry.delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1693
+ let mutated = false;
1694
+ for (const entry of rawEntries) {
1695
+ if (entry.delivery.short_id?.trim()) {
1696
+ continue;
1697
+ }
1698
+ entry.delivery.short_id = generateStableShortId(root, "delivery", entry.delivery.id, used);
1699
+ used.push(entry.delivery.short_id);
1700
+ writeDeliveryFile(entry.filePath, entry.delivery, entry.raw, entry.body);
1701
+ mutated = true;
1702
+ }
1703
+ return mutated ? rawEntries.map((entry) => {
1704
+ const parsed = parseDeliveryFile(entry.filePath);
1705
+ return { delivery: parsed.delivery, filePath: entry.filePath, body: parsed.body, raw: parsed.raw };
1706
+ }) : rawEntries;
1707
+ }
1708
+ function resolveUniqueShortId(entries, value) {
1709
+ const normalized = value.trim().toLowerCase();
1710
+ if (!normalized) return void 0;
1711
+ const exact = entries.find((entry) => entry.short_id?.toLowerCase() === normalized);
1712
+ if (exact) return exact;
1713
+ if (normalized.length < 6) return void 0;
1714
+ const matches = entries.filter((entry) => entry.short_id?.toLowerCase().startsWith(normalized));
1715
+ if (matches.length === 1) {
1716
+ return matches[0];
1717
+ }
1718
+ if (matches.length > 1) {
1719
+ throw new Error(`Ambiguous short id '${value}'. Candidates: ${matches.map((entry) => entry.id).join(", ")}.`);
1720
+ }
1721
+ return void 0;
1722
+ }
1723
+ function resolveExistingTrackId(root, trackId) {
1724
+ const normalized = trackId.trim().toLowerCase();
1725
+ if (!normalized) return void 0;
1726
+ if (normalized === "unassigned") return "unassigned";
1727
+ const entries = loadTrackEntries(root);
1728
+ const byId = entries.find((entry) => entry.track.id.trim().toLowerCase() === normalized);
1729
+ if (byId) return byId.track.id.trim();
1730
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.track), normalized);
1731
+ if (byShort) return byShort.id.trim();
1732
+ const byName = entries.filter((entry) => entry.track.name.trim().toLowerCase() === normalized);
1733
+ if (byName.length === 1) return byName[0].track.id.trim();
1734
+ if (byName.length > 1) {
1735
+ throw new Error(`Multiple tracks match '${trackId}'. Use the track id or short id.`);
1736
+ }
1737
+ return void 0;
1738
+ }
1739
+ function resolveExistingDeliveryId(root, deliveryId) {
1740
+ const normalized = deliveryId.trim().toLowerCase();
1741
+ if (!normalized) return void 0;
1742
+ const entries = loadDeliveryEntries(root);
1743
+ const byId = entries.find((entry) => entry.delivery.id.trim().toLowerCase() === normalized);
1744
+ if (byId) return byId.delivery.id.trim();
1745
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), normalized);
1746
+ if (byShort) return byShort.id.trim();
1747
+ const byName = entries.filter((entry) => entry.delivery.name.trim().toLowerCase() === normalized);
1748
+ if (byName.length === 1) return byName[0].delivery.id.trim();
1749
+ if (byName.length > 1) {
1750
+ throw new Error(`Multiple deliveries match '${deliveryId}'. Use the delivery id or short id.`);
1751
+ }
1752
+ return void 0;
1753
+ }
1754
+ function assertExistingTrackId(root, trackId) {
1755
+ const resolved = resolveExistingTrackId(root, trackId);
1756
+ if (resolved) {
1757
+ return resolved;
1758
+ }
1759
+ throw new Error(
1760
+ `Unknown track '${trackId}'. Create it first with \`coop create track --id ${trackId} --name "${trackId}"\` or use \`unassigned\`.`
1761
+ );
1762
+ }
1763
+ function assertExistingDeliveryId(root, deliveryId) {
1764
+ const resolved = resolveExistingDeliveryId(root, deliveryId);
1765
+ if (resolved) {
1766
+ return resolved;
1767
+ }
1768
+ throw new Error(`Unknown delivery '${deliveryId}'. Create it first with \`coop create delivery --id ${deliveryId} --name "${deliveryId}"\`.`);
1769
+ }
1770
+ function unique(values) {
1771
+ return Array.from(new Set(values));
1772
+ }
1773
+ function normalizeTaskReferences(root, task) {
1774
+ const next = { ...task };
1775
+ if (next.track?.trim()) {
1776
+ next.track = assertExistingTrackId(root, next.track.trim());
1777
+ }
1778
+ if (next.delivery?.trim()) {
1779
+ next.delivery = assertExistingDeliveryId(root, next.delivery.trim());
1780
+ }
1781
+ if (next.delivery_tracks?.length) {
1782
+ next.delivery_tracks = unique(next.delivery_tracks.map((trackId) => assertExistingTrackId(root, trackId)));
1783
+ }
1784
+ if (next.priority_context && typeof next.priority_context === "object") {
1785
+ const normalized = {};
1786
+ for (const [trackId, priority] of Object.entries(next.priority_context)) {
1787
+ normalized[assertExistingTrackId(root, trackId)] = priority;
1788
+ }
1789
+ next.priority_context = normalized;
1790
+ }
1791
+ return next;
1792
+ }
1402
1793
  function resolveTaskFile(root, idOrAlias) {
1403
1794
  const reference = resolveReference(root, idOrAlias, "task");
1404
1795
  return path3.join(root, ...reference.file.split("/"));
@@ -1409,27 +1800,45 @@ function resolveIdeaFile(root, idOrAlias) {
1409
1800
  }
1410
1801
  function loadTaskEntry(root, idOrAlias) {
1411
1802
  const filePath = resolveTaskFile(root, idOrAlias);
1412
- return { filePath, parsed: parseTaskFile4(filePath) };
1803
+ const parsed = parseTaskFile4(filePath);
1804
+ if (!parsed.task.short_id?.trim()) {
1805
+ const shortId = generateStableShortId(root, "task", parsed.task.id);
1806
+ const nextTask = { ...parsed.task, short_id: shortId };
1807
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1808
+ writeTask3(nextTask, { body: parsed.body, raw: nextRaw, filePath });
1809
+ return { filePath, parsed: parseTaskFile4(filePath) };
1810
+ }
1811
+ return { filePath, parsed };
1413
1812
  }
1414
1813
  function loadIdeaEntry(root, idOrAlias) {
1415
1814
  const filePath = resolveIdeaFile(root, idOrAlias);
1416
- return { filePath, parsed: parseIdeaFile2(filePath) };
1815
+ const parsed = parseIdeaFile2(filePath);
1816
+ if (!parsed.idea.short_id?.trim()) {
1817
+ const shortId = generateStableShortId(root, "idea", parsed.idea.id);
1818
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1819
+ const output2 = stringifyFrontmatter2(nextRaw, parsed.body);
1820
+ fs3.writeFileSync(filePath, output2, "utf8");
1821
+ return { filePath, parsed: parseIdeaFile2(filePath) };
1822
+ }
1823
+ return { filePath, parsed };
1417
1824
  }
1418
1825
  function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
1419
1826
  const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
1420
1827
  fs3.writeFileSync(filePath, output2, "utf8");
1421
1828
  }
1422
- function validateTaskForWrite(task, filePath) {
1423
- const structuralIssues = validateStructural2(task, { filePath });
1424
- const semanticIssues = validateSemantic(task);
1829
+ function validateTaskForWrite(root, task, filePath) {
1830
+ const normalized = normalizeTaskReferences(root, task);
1831
+ const structuralIssues = validateStructural2(normalized, { filePath });
1832
+ const semanticIssues = validateSemantic(normalized);
1425
1833
  const errors = [...structuralIssues, ...semanticIssues].filter((issue) => issue.level === "error");
1426
1834
  if (errors.length > 0) {
1427
1835
  throw new Error(errors.map((issue) => `- ${issue.message}`).join("\n"));
1428
1836
  }
1837
+ return normalized;
1429
1838
  }
1430
- function writeTaskEntry(filePath, parsed, task, body = parsed.body) {
1431
- validateTaskForWrite(task, filePath);
1432
- writeTask3(task, {
1839
+ function writeTaskEntry(root, filePath, parsed, task, body = parsed.body) {
1840
+ const normalized = validateTaskForWrite(root, task, filePath);
1841
+ writeTask3(normalized, {
1433
1842
  body,
1434
1843
  raw: parsed.raw,
1435
1844
  filePath
@@ -1493,18 +1902,18 @@ function taskEffectivePriority(task, track) {
1493
1902
  return effective_priority(task, track);
1494
1903
  }
1495
1904
  function resolveDeliveryEntry(root, ref) {
1496
- const files = listDeliveryFiles(root);
1497
1905
  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
- });
1906
+ const entries = loadDeliveryEntries(root);
1502
1907
  const direct = entries.find((entry) => entry.delivery.id.toLowerCase() === target);
1503
1908
  if (direct) return direct;
1909
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), target);
1910
+ if (byShort) {
1911
+ return entries.find((entry) => entry.delivery.id === byShort.id);
1912
+ }
1504
1913
  const byName = entries.filter((entry) => entry.delivery.name.toLowerCase() === target);
1505
1914
  if (byName.length === 1) return byName[0];
1506
1915
  if (byName.length > 1) {
1507
- throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id.`);
1916
+ throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id or short id.`);
1508
1917
  }
1509
1918
  throw new Error(`Delivery '${ref}' not found.`);
1510
1919
  }
@@ -1613,10 +2022,12 @@ function configDefaultVersion(root) {
1613
2022
  }
1614
2023
  function resolveSelectionOptions(root, options) {
1615
2024
  const context = readWorkingContext(root, resolveCoopHome());
2025
+ const track = options.track?.trim() || context.track?.trim() || configDefaultTrack(root);
2026
+ const delivery = options.delivery?.trim() || context.delivery?.trim() || configDefaultDelivery(root);
1616
2027
  return {
1617
2028
  ...options,
1618
- track: options.track?.trim() || context.track?.trim() || configDefaultTrack(root),
1619
- delivery: options.delivery?.trim() || context.delivery?.trim() || configDefaultDelivery(root),
2029
+ track: track ? resolveExistingTrackId(root, track) ?? track : void 0,
2030
+ delivery: delivery ? resolveExistingDeliveryId(root, delivery) ?? delivery : void 0,
1620
2031
  version: options.version?.trim() || context.version?.trim() || configDefaultVersion(root)
1621
2032
  };
1622
2033
  }
@@ -1641,6 +2052,7 @@ function selectTopReadyTask(root = resolveRepoRoot(), options = {}) {
1641
2052
  function formatSelectedTask(entry, selection = {}) {
1642
2053
  const lines = [
1643
2054
  `Selected task: ${entry.task.id}`,
2055
+ `Short ID: ${entry.task.short_id ?? "-"}`,
1644
2056
  `Title: ${entry.task.title}`,
1645
2057
  `Priority: ${selection.track && entry.task.priority_context?.[selection.track] ? `${entry.task.priority ?? "-"} -> ${entry.task.priority_context[selection.track]}` : entry.task.priority ?? "-"}`,
1646
2058
  `Track: ${entry.task.track ?? "-"}`,
@@ -1885,7 +2297,7 @@ function registerCommentCommand(program) {
1885
2297
  const root = resolveRepoRoot();
1886
2298
  const { filePath, parsed } = loadTaskEntry(root, id);
1887
2299
  const task = appendTaskComment(parsed.task, options.author?.trim() || defaultCoopAuthor(root), options.message.trim());
1888
- writeTaskEntry(filePath, parsed, task);
2300
+ writeTaskEntry(root, filePath, parsed, task);
1889
2301
  console.log(`Commented ${task.id}`);
1890
2302
  });
1891
2303
  }
@@ -1963,7 +2375,9 @@ function writeIdNamingValue(root, value) {
1963
2375
  const config = readCoopConfig(root).raw;
1964
2376
  const next = { ...config };
1965
2377
  const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
1966
- idRaw.naming = nextValue;
2378
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
2379
+ namingRaw.task = nextValue;
2380
+ idRaw.naming = namingRaw;
1967
2381
  next.id = idRaw;
1968
2382
  writeCoopConfig(root, next);
1969
2383
  }
@@ -2134,10 +2548,11 @@ import {
2134
2548
  TaskType as TaskType2,
2135
2549
  check_permission as check_permission2,
2136
2550
  load_auth_config as load_auth_config2,
2551
+ parseDeliveryFile as parseDeliveryFile2,
2137
2552
  parseTaskFile as parseTaskFile7,
2138
- writeYamlFile as writeYamlFile3,
2553
+ parseYamlFile as parseYamlFile3,
2554
+ writeYamlFile as writeYamlFile4,
2139
2555
  stringifyFrontmatter as stringifyFrontmatter4,
2140
- validateStructural as validateStructural5,
2141
2556
  writeTask as writeTask6
2142
2557
  } from "@kitsy/coop-core";
2143
2558
  import { create_provider_idea_decomposer, decompose_idea_to_tasks } from "@kitsy/coop-ai";
@@ -2166,7 +2581,7 @@ function renderSelect(question, choices, selected) {
2166
2581
  process2.stdout.write(` ${prefix} ${choice.label}${hint}
2167
2582
  `);
2168
2583
  }
2169
- process2.stdout.write("\nUse \u2191/\u2193 to choose, Enter to confirm.\n");
2584
+ process2.stdout.write("\nUse Up/Down arrows to choose, Enter to confirm.\n");
2170
2585
  }
2171
2586
  function moveCursorUp(lines) {
2172
2587
  if (lines <= 0) return;
@@ -2182,7 +2597,9 @@ async function select(question, choices, defaultIndex = 0) {
2182
2597
  }
2183
2598
  readline.emitKeypressEvents(process2.stdin);
2184
2599
  const previousRawMode = process2.stdin.isRaw;
2600
+ const wasPaused = typeof process2.stdin.isPaused === "function" ? process2.stdin.isPaused() : false;
2185
2601
  process2.stdin.setRawMode(true);
2602
+ process2.stdin.resume();
2186
2603
  let selected = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
2187
2604
  const renderedLines = choices.length + 2;
2188
2605
  renderSelect(question, choices, selected);
@@ -2190,6 +2607,9 @@ async function select(question, choices, defaultIndex = 0) {
2190
2607
  const cleanup = () => {
2191
2608
  process2.stdin.off("keypress", onKeypress);
2192
2609
  process2.stdin.setRawMode(previousRawMode ?? false);
2610
+ if (wasPaused) {
2611
+ process2.stdin.pause();
2612
+ }
2193
2613
  process2.stdout.write("\n");
2194
2614
  };
2195
2615
  const rerender = () => {
@@ -2631,15 +3051,50 @@ function plusDaysIso(days) {
2631
3051
  date.setUTCDate(date.getUTCDate() + days);
2632
3052
  return date.toISOString().slice(0, 10);
2633
3053
  }
2634
- function unique(values) {
3054
+ function unique2(values) {
2635
3055
  return Array.from(new Set(values));
2636
3056
  }
3057
+ function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
3058
+ const normalizedName = candidateName.trim().toLowerCase();
3059
+ if (!normalizedName) {
3060
+ return;
3061
+ }
3062
+ const conflict = entries.find(
3063
+ (entry) => entry.id.trim().toLowerCase() !== candidateId.trim().toLowerCase() && entry.name.trim().toLowerCase() === normalizedName
3064
+ );
3065
+ if (!conflict) {
3066
+ return;
3067
+ }
3068
+ throw new Error(
3069
+ `${kind === "track" ? "Track" : "Delivery"} name '${candidateName}' conflicts with existing ${kind} '${conflict.id}' (${conflict.name}). Names are matched case-insensitively.`
3070
+ );
3071
+ }
3072
+ function taskShortIds(root) {
3073
+ return listTaskFiles(root).map((filePath) => parseTaskFile7(filePath).task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3074
+ }
3075
+ function ideaShortIds(root) {
3076
+ return listIdeaFiles(root).map((filePath) => parseIdeaFile3(filePath).idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3077
+ }
3078
+ function trackShortIds(root) {
3079
+ return listTrackFiles(root).map((filePath) => parseYamlFile3(filePath).short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3080
+ }
3081
+ function deliveryShortIds(root) {
3082
+ return listDeliveryFiles(root).map((filePath) => parseDeliveryFile2(filePath).delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3083
+ }
3084
+ function assertKnownDynamicFields(root, fields) {
3085
+ const tokens = namingTokensForRoot(root);
3086
+ for (const key of Object.keys(fields)) {
3087
+ if (!tokens[key]) {
3088
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
3089
+ }
3090
+ }
3091
+ }
2637
3092
  function resolveIdeaFile2(root, idOrAlias) {
2638
3093
  const target = resolveReference(root, idOrAlias, "idea");
2639
3094
  return path8.join(root, ...target.file.split("/"));
2640
3095
  }
2641
3096
  function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
2642
- const next = unique([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
3097
+ const next = unique2([...idea.linked_tasks ?? [], ...linked]).sort((a, b) => a.localeCompare(b));
2643
3098
  const nextRaw = {
2644
3099
  ...raw,
2645
3100
  linked_tasks: next
@@ -2654,18 +3109,41 @@ function makeTaskDraft(input2) {
2654
3109
  track: input2.track,
2655
3110
  priority: input2.priority,
2656
3111
  body: input2.body,
2657
- acceptance: unique(input2.acceptance ?? []),
2658
- testsRequired: unique(input2.testsRequired ?? []),
2659
- authorityRefs: unique(input2.authorityRefs ?? []),
2660
- derivedRefs: unique(input2.derivedRefs ?? [])
3112
+ acceptance: unique2(input2.acceptance ?? []),
3113
+ testsRequired: unique2(input2.testsRequired ?? []),
3114
+ authorityRefs: unique2(input2.authorityRefs ?? []),
3115
+ derivedRefs: unique2(input2.derivedRefs ?? [])
2661
3116
  };
2662
3117
  }
2663
3118
  function registerCreateCommand(program) {
2664
3119
  const create = program.command("create").description("Create COOP entities");
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) => {
3120
+ create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().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) => {
2666
3121
  const root = resolveRepoRoot();
2667
3122
  const coop = ensureCoopInitialized(root);
2668
3123
  const interactive = Boolean(options.interactive);
3124
+ const dynamicFields = extractDynamicTokenFlags(
3125
+ ["create", "task"],
3126
+ [
3127
+ "id",
3128
+ "from",
3129
+ "title",
3130
+ "type",
3131
+ "status",
3132
+ "track",
3133
+ "delivery",
3134
+ "priority",
3135
+ "body",
3136
+ "acceptance",
3137
+ "tests-required",
3138
+ "authority-ref",
3139
+ "derived-ref",
3140
+ "from-file",
3141
+ "stdin",
3142
+ "interactive",
3143
+ "ai"
3144
+ ]
3145
+ );
3146
+ assertKnownDynamicFields(root, dynamicFields);
2669
3147
  if (options.fromFile?.trim() || options.stdin) {
2670
3148
  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) {
2671
3149
  throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
@@ -2691,10 +3169,10 @@ function registerCreateCommand(program) {
2691
3169
  const priority = options.priority?.trim() || (interactive ? await ask("Priority", "p2") : "p2");
2692
3170
  const delivery = options.delivery?.trim() || (interactive ? await ask("Delivery (optional)", "") : "");
2693
3171
  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)", "")) : [];
3172
+ const acceptance = options.acceptance && options.acceptance.length > 0 ? unique2(options.acceptance) : interactive ? parseCsv(await ask("Acceptance criteria (comma-separated, optional)", "")) : [];
3173
+ const testsRequired = options.testsRequired && options.testsRequired.length > 0 ? unique2(options.testsRequired) : interactive ? parseCsv(await ask("Tests required (comma-separated, optional)", "")) : [];
3174
+ const authorityRefs = options.authorityRef && options.authorityRef.length > 0 ? unique2(options.authorityRef) : interactive ? parseCsv(await ask("Authority refs (comma-separated, optional)", "")) : [];
3175
+ const derivedRefs = options.derivedRef && options.derivedRef.length > 0 ? unique2(options.derivedRef) : interactive ? parseCsv(await ask("Derived refs (comma-separated, optional)", "")) : [];
2698
3176
  const date = todayIsoDate();
2699
3177
  const drafts = [];
2700
3178
  let sourceIdeaPath = null;
@@ -2775,23 +3253,28 @@ function registerCreateCommand(program) {
2775
3253
  }
2776
3254
  const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
2777
3255
  const createdIds = [];
3256
+ const existingShortIds = taskShortIds(root);
3257
+ const createdShortIds = [];
2778
3258
  for (let index = 0; index < drafts.length; index += 1) {
2779
3259
  const draft = drafts[index];
2780
3260
  if (!draft) continue;
2781
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, [...existingIds, ...createdIds], {
3261
+ const id = (options.id?.trim() ? normalizeEntityId("task", options.id) : void 0) || generateConfiguredId(root, [...existingIds, ...createdIds], {
2782
3262
  entityType: "task",
2783
3263
  title: draft.title,
2784
3264
  taskType: draft.type,
2785
3265
  track: draft.track,
2786
3266
  status: draft.status,
3267
+ name: draft.title,
2787
3268
  fields: {
2788
3269
  track: draft.track,
2789
3270
  type: draft.type,
2790
- feature: draft.track || draft.type
3271
+ feature: draft.track || draft.type,
3272
+ ...dynamicFields
2791
3273
  }
2792
3274
  });
2793
3275
  const task = {
2794
3276
  id,
3277
+ short_id: generateStableShortId(root, "task", id, [...existingShortIds, ...createdShortIds]),
2795
3278
  title: draft.title,
2796
3279
  type: draft.type,
2797
3280
  status: draft.status,
@@ -2810,17 +3293,15 @@ function registerCreateCommand(program) {
2810
3293
  } : void 0
2811
3294
  };
2812
3295
  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, {
3296
+ const normalizedTask = validateTaskForWrite(root, task, filePath);
3297
+ writeTask6(normalizedTask, {
2820
3298
  body: draft.body,
2821
3299
  filePath
2822
3300
  });
2823
3301
  createdIds.push(id);
3302
+ if (normalizedTask.short_id) {
3303
+ createdShortIds.push(normalizedTask.short_id);
3304
+ }
2824
3305
  console.log(`Created task: ${path8.relative(root, filePath)}`);
2825
3306
  }
2826
3307
  if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
@@ -2834,10 +3315,15 @@ ${message}`);
2834
3315
  console.log(`Linked ${createdIds.length} task(s) to idea: ${sourceIdeaParsed.idea.id}`);
2835
3316
  }
2836
3317
  });
2837
- create.command("idea").description("Create an idea").argument("[title]", "Idea title").option("--id <id>", "Idea id").option("--title <title>", "Idea title").option("--author <author>", "Idea author").option("--source <source>", "Idea source").option("--status <status>", `Idea status (${Object.values(IdeaStatus2).join(", ")})`).option("--tags <tags>", "Comma-separated tags").option("--body <body>", "Markdown body").option("--from-file <path>", "Create an idea from an idea draft file").option("--stdin", "Read idea draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
3318
+ create.command("idea").description("Create an idea").allowUnknownOption().allowExcessArguments().argument("[title]", "Idea title").option("--id <id>", "Idea id").option("--title <title>", "Idea title").option("--author <author>", "Idea author").option("--source <source>", "Idea source").option("--status <status>", `Idea status (${Object.values(IdeaStatus2).join(", ")})`).option("--tags <tags>", "Comma-separated tags").option("--body <body>", "Markdown body").option("--from-file <path>", "Create an idea from an idea draft file").option("--stdin", "Read idea draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
2838
3319
  const root = resolveRepoRoot();
2839
3320
  const coop = ensureCoopInitialized(root);
2840
3321
  const interactive = Boolean(options.interactive);
3322
+ const dynamicFields = extractDynamicTokenFlags(
3323
+ ["create", "idea"],
3324
+ ["id", "title", "author", "source", "status", "tags", "body", "from-file", "stdin", "interactive"]
3325
+ );
3326
+ assertKnownDynamicFields(root, dynamicFields);
2841
3327
  if (options.fromFile?.trim() || options.stdin) {
2842
3328
  if (options.id || options.title || titleArg || options.author || options.source || options.status || options.tags || options.body) {
2843
3329
  throw new Error("Cannot combine --from-file/--stdin with direct idea field flags. Use one input mode.");
@@ -2859,17 +3345,21 @@ ${message}`);
2859
3345
  const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
2860
3346
  const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
2861
3347
  const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
2862
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3348
+ const id = (options.id?.trim() ? normalizeEntityId("idea", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2863
3349
  entityType: "idea",
2864
3350
  title,
3351
+ name: title,
2865
3352
  status,
2866
3353
  fields: {
2867
3354
  source,
2868
- author
3355
+ author,
3356
+ ...dynamicFields
2869
3357
  }
2870
3358
  });
3359
+ const shortId = generateStableShortId(root, "idea", id, ideaShortIds(root));
2871
3360
  const frontmatter = {
2872
3361
  id,
3362
+ short_id: shortId,
2873
3363
  title,
2874
3364
  created: todayIsoDate(),
2875
3365
  aliases: [],
@@ -2886,13 +3376,18 @@ ${message}`);
2886
3376
  fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
2887
3377
  console.log(`Created idea: ${path8.relative(root, filePath)}`);
2888
3378
  });
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) => {
3379
+ create.command("track").description("Create a track").allowUnknownOption().allowExcessArguments().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) => {
2890
3380
  const root = resolveRepoRoot();
2891
3381
  const coop = ensureCoopInitialized(root);
2892
3382
  const interactive = Boolean(options.interactive);
3383
+ const dynamicFields = extractDynamicTokenFlags(
3384
+ ["create", "track"],
3385
+ ["id", "name", "profiles", "max-wip", "allowed-types", "interactive"]
3386
+ );
3387
+ assertKnownDynamicFields(root, dynamicFields);
2893
3388
  const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
2894
3389
  if (!name) throw new Error("Track name is required.");
2895
- const capacityProfiles = unique(
3390
+ const capacityProfiles = unique2(
2896
3391
  options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
2897
3392
  );
2898
3393
  const maxWipInput = options.maxWip?.trim() || (interactive ? await ask("Max concurrent tasks", "6") : "6");
@@ -2900,7 +3395,7 @@ ${message}`);
2900
3395
  if (typeof maxWip !== "number" || maxWip <= 0 || !Number.isInteger(maxWip)) {
2901
3396
  throw new Error("max-wip must be a positive integer.");
2902
3397
  }
2903
- const allowed = unique(
3398
+ const allowed = unique2(
2904
3399
  options.allowedTypes ? parseCsv(options.allowedTypes).map((entry) => entry.toLowerCase()) : interactive ? parseCsv(await ask("Allowed types (comma-separated)", "feature,bug,chore,spike")).map(
2905
3400
  (entry) => entry.toLowerCase()
2906
3401
  ) : ["feature", "bug", "chore", "spike"]
@@ -2911,19 +3406,21 @@ ${message}`);
2911
3406
  }
2912
3407
  }
2913
3408
  const existingIds = listTrackFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml)$/i, ""));
2914
- const config = readCoopConfig(root);
2915
- const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
2916
- const prefix = typeof idPrefixesRaw.track === "string" ? idPrefixesRaw.track : "TRK";
2917
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3409
+ const id = (options.id?.trim() ? normalizeEntityId("track", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2918
3410
  entityType: "track",
2919
- prefix,
2920
3411
  title: name,
3412
+ name,
2921
3413
  fields: {
2922
- name
3414
+ name,
3415
+ ...dynamicFields
2923
3416
  }
2924
3417
  });
3418
+ const existingTracks = listTrackFiles(root).map((filePath2) => parseYamlFile3(filePath2));
3419
+ assertNoCaseInsensitiveNameConflict("track", existingTracks, id, name);
3420
+ const shortId = generateStableShortId(root, "track", id, trackShortIds(root));
2925
3421
  const payload = {
2926
3422
  id,
3423
+ short_id: shortId,
2927
3424
  name,
2928
3425
  capacity_profiles: capacityProfiles,
2929
3426
  constraints: {
@@ -2932,13 +3429,32 @@ ${message}`);
2932
3429
  }
2933
3430
  };
2934
3431
  const filePath = path8.join(coop, "tracks", `${id}.yml`);
2935
- writeYamlFile3(filePath, payload);
3432
+ writeYamlFile4(filePath, payload);
2936
3433
  console.log(`Created track: ${path8.relative(root, filePath)}`);
2937
3434
  });
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) => {
3435
+ create.command("delivery").description("Create a delivery").allowUnknownOption().allowExcessArguments().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) => {
2939
3436
  const root = resolveRepoRoot();
2940
3437
  const coop = ensureCoopInitialized(root);
2941
3438
  const interactive = Boolean(options.interactive);
3439
+ const dynamicFields = extractDynamicTokenFlags(
3440
+ ["create", "delivery"],
3441
+ [
3442
+ "id",
3443
+ "name",
3444
+ "status",
3445
+ "commit",
3446
+ "target-date",
3447
+ "budget-hours",
3448
+ "budget-cost",
3449
+ "profiles",
3450
+ "scope",
3451
+ "exclude",
3452
+ "user",
3453
+ "force",
3454
+ "interactive"
3455
+ ]
3456
+ );
3457
+ assertKnownDynamicFields(root, dynamicFields);
2942
3458
  const user = options.user?.trim() || defaultCoopAuthor(root);
2943
3459
  const config = readCoopConfig(root);
2944
3460
  const auth = load_auth_config2(config.raw);
@@ -2982,7 +3498,7 @@ ${message}`);
2982
3498
  options.budgetCost?.trim() || (interactive ? await ask("Budget cost USD (optional)", "") : void 0),
2983
3499
  "budget-cost"
2984
3500
  );
2985
- const capacityProfiles = unique(
3501
+ const capacityProfiles = unique2(
2986
3502
  options.profiles ? parseCsv(options.profiles) : interactive ? parseCsv(await ask("Capacity profiles (comma-separated)", "backend_team")) : ["backend_team"]
2987
3503
  );
2988
3504
  const tasks = listTaskFiles(root).map((filePath2) => {
@@ -2995,12 +3511,12 @@ ${message}`);
2995
3511
  console.log(`- ${task.id}: ${task.title}`);
2996
3512
  }
2997
3513
  }
2998
- const scopeInclude = unique(
3514
+ const scopeInclude = unique2(
2999
3515
  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
3516
  (value) => value.toUpperCase()
3001
3517
  ) : []
3002
3518
  );
3003
- const scopeExclude = unique(
3519
+ const scopeExclude = unique2(
3004
3520
  options.exclude ? parseCsv(options.exclude).map((value) => value.toUpperCase()) : interactive ? parseCsv(await ask("Scope exclude task IDs (comma-separated)", "")).map((value) => value.toUpperCase()) : []
3005
3521
  );
3006
3522
  const knownTaskIds = new Set(tasks.map((task) => task.id));
@@ -3014,21 +3530,22 @@ ${message}`);
3014
3530
  throw new Error(`Scope exclude task '${id2}' does not exist.`);
3015
3531
  }
3016
3532
  }
3017
- const existingIds = listDeliveryFiles(root).map(
3018
- (filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, "")
3019
- );
3020
- const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
3021
- const prefix = typeof idPrefixesRaw.delivery === "string" ? idPrefixesRaw.delivery : "DEL";
3022
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3533
+ const existingIds = listDeliveryFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, ""));
3534
+ const id = (options.id?.trim() ? normalizeEntityId("delivery", options.id) : void 0) || generateConfiguredId(root, existingIds, {
3023
3535
  entityType: "delivery",
3024
- prefix,
3025
3536
  title: name,
3537
+ name,
3026
3538
  fields: {
3027
- status
3539
+ status,
3540
+ ...dynamicFields
3028
3541
  }
3029
3542
  });
3543
+ const existingDeliveries = listDeliveryFiles(root).map((filePath2) => parseDeliveryFile2(filePath2).delivery);
3544
+ assertNoCaseInsensitiveNameConflict("delivery", existingDeliveries, id, name);
3545
+ const shortId = generateStableShortId(root, "delivery", id, deliveryShortIds(root));
3030
3546
  const payload = {
3031
3547
  id,
3548
+ short_id: shortId,
3032
3549
  name,
3033
3550
  status,
3034
3551
  target_date: targetDate,
@@ -3045,7 +3562,7 @@ ${message}`);
3045
3562
  }
3046
3563
  };
3047
3564
  const filePath = path8.join(coop, "deliveries", `${id}.yml`);
3048
- writeYamlFile3(filePath, payload);
3565
+ writeYamlFile4(filePath, payload);
3049
3566
  console.log(`Created delivery: ${path8.relative(root, filePath)}`);
3050
3567
  });
3051
3568
  }
@@ -3336,6 +3853,8 @@ var catalog = {
3336
3853
  selection_rules: [
3337
3854
  "Use `coop project show` first to confirm the active workspace and project.",
3338
3855
  "Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
3856
+ "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`.",
3857
+ "Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
3339
3858
  "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
3859
  "Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
3341
3860
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
@@ -3393,9 +3912,14 @@ var catalog = {
3393
3912
  { usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
3394
3913
  { usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
3395
3914
  { usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
3915
+ { usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
3916
+ { usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
3396
3917
  { usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
3397
- { usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
3398
- { usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
3918
+ { usage: "coop naming", purpose: "Explain the effective per-entity naming rules, custom tokens, and examples." },
3919
+ { usage: 'coop naming preview "Natural-language COOP command recommender" --entity task', purpose: "Preview the generated ID before creating an item." },
3920
+ { usage: "coop naming set task <TYPE>-<TITLE16>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand." },
3921
+ { usage: "coop naming token create proj", purpose: "Create a custom naming token." },
3922
+ { usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." }
3399
3923
  ]
3400
3924
  },
3401
3925
  {
@@ -3406,7 +3930,10 @@ var catalog = {
3406
3930
  { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
3407
3931
  { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
3408
3932
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3933
+ { usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
3934
+ { usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
3409
3935
  { usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
3936
+ { usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
3410
3937
  {
3411
3938
  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
3939
  purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
@@ -3450,6 +3977,8 @@ var catalog = {
3450
3977
  description: "Read backlog state, task details, and planning output.",
3451
3978
  commands: [
3452
3979
  { usage: "coop list tasks --status todo", purpose: "List tasks with filters." },
3980
+ { usage: "coop list tracks", purpose: "List valid named tracks." },
3981
+ { usage: "coop list deliveries", purpose: "List valid named deliveries." },
3453
3982
  { 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
3983
  { usage: "coop list tasks --mine", purpose: "List tasks assigned to the current default COOP author." },
3455
3984
  { usage: 'coop search "auth and login form"', purpose: "Run deterministic non-AI search across tasks, ideas, and deliveries." },
@@ -3548,9 +4077,10 @@ function renderTopicPayload(topic) {
3548
4077
  return {
3549
4078
  topic,
3550
4079
  naming_guidance: [
3551
- "Use `coop naming` to inspect the current naming template and token behavior.",
3552
- 'Use `coop naming preview "<title>"` before creating a new idea, task, or delivery if predictable IDs matter.',
3553
- "Use `coop config id.naming ...` to override the default semantic naming template."
4080
+ "Use `coop naming` to inspect per-entity templates, custom tokens, and examples.",
4081
+ 'Use `coop naming preview "<title>" --entity <entity>` before creating a new item if predictable IDs matter.',
4082
+ "Use `coop naming set <entity> <template>` to update one entity's naming rule.",
4083
+ "Use `coop naming token create <token>` and `coop naming token value add <token> <value>` before passing custom token flags like `--proj UX`."
3554
4084
  ]
3555
4085
  };
3556
4086
  }
@@ -3834,11 +4364,11 @@ function resolveHelpTopic(options) {
3834
4364
  if (options.naming) {
3835
4365
  requestedTopics.push("naming");
3836
4366
  }
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(", ")}.`);
4367
+ const unique4 = [...new Set(requestedTopics)];
4368
+ if (unique4.length > 1) {
4369
+ throw new Error(`Specify only one focused help-ai topic at a time. Received: ${unique4.join(", ")}.`);
3840
4370
  }
3841
- const topic = unique3[0];
4371
+ const topic = unique4[0];
3842
4372
  if (topic !== void 0 && topic !== "state-transitions" && topic !== "artifacts" && topic !== "post-execution" && topic !== "selection" && topic !== "naming") {
3843
4373
  throw new Error(`Unsupported help-ai topic '${topic}'. Expected state-transitions|artifacts|post-execution|selection|naming.`);
3844
4374
  }
@@ -3904,7 +4434,7 @@ import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
3904
4434
  import fs8 from "fs";
3905
4435
  import path11 from "path";
3906
4436
  import { spawnSync as spawnSync3 } from "child_process";
3907
- import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural6 } from "@kitsy/coop-core";
4437
+ import { detect_cycle, parseTaskContent, parseTaskFile as parseTaskFile8, validateStructural as validateStructural5 } from "@kitsy/coop-core";
3908
4438
  var HOOK_BLOCK_START = "# COOP_PRE_COMMIT_START";
3909
4439
  var HOOK_BLOCK_END = "# COOP_PRE_COMMIT_END";
3910
4440
  function runGit(repoRoot, args, allowFailure = false) {
@@ -3988,7 +4518,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
3988
4518
  errors.push(`[COOP] ${message}`);
3989
4519
  continue;
3990
4520
  }
3991
- const issues = validateStructural6(task, { filePath: absolutePath });
4521
+ const issues = validateStructural5(task, { filePath: absolutePath });
3992
4522
  for (const issue of issues) {
3993
4523
  errors.push(`[COOP] ${relativePath}: ${issue.message}`);
3994
4524
  }
@@ -4278,7 +4808,13 @@ id_prefixes:
4278
4808
  run: "RUN"
4279
4809
 
4280
4810
  id:
4281
- naming: ${JSON.stringify(namingTemplate)}
4811
+ naming:
4812
+ task: ${JSON.stringify(namingTemplate)}
4813
+ idea: ${JSON.stringify(namingTemplate)}
4814
+ track: "<NAME_SLUG>"
4815
+ delivery: "<NAME_SLUG>"
4816
+ run: "<TYPE>-<YYMMDD>-<RAND>"
4817
+ tokens: {}
4282
4818
  seq_padding: 0
4283
4819
 
4284
4820
  defaults:
@@ -4640,12 +5176,51 @@ import path15 from "path";
4640
5176
  import {
4641
5177
  effective_priority as effective_priority3,
4642
5178
  load_graph as load_graph6,
4643
- parseDeliveryFile as parseDeliveryFile2,
4644
5179
  parseIdeaFile as parseIdeaFile4,
4645
5180
  parseTaskFile as parseTaskFile10,
4646
5181
  schedule_next as schedule_next3
4647
5182
  } from "@kitsy/coop-core";
4648
5183
  import chalk2 from "chalk";
5184
+ var TASK_COLUMN_WIDTHS = {
5185
+ id: 24,
5186
+ short: 12,
5187
+ title: 30,
5188
+ status: 12,
5189
+ priority: 4,
5190
+ assignee: 16,
5191
+ track: 16,
5192
+ delivery: 16,
5193
+ score: 6,
5194
+ file: 30
5195
+ };
5196
+ var IDEA_COLUMN_WIDTHS = {
5197
+ id: 24,
5198
+ short: 12,
5199
+ title: 30,
5200
+ status: 12,
5201
+ file: 30
5202
+ };
5203
+ function truncateCell(value, maxWidth) {
5204
+ if (maxWidth <= 0 || value.length <= maxWidth) {
5205
+ return value;
5206
+ }
5207
+ if (maxWidth <= 3) {
5208
+ return value.slice(0, maxWidth);
5209
+ }
5210
+ return `${value.slice(0, maxWidth - 3)}...`;
5211
+ }
5212
+ function truncateMiddleCell(value, maxWidth) {
5213
+ if (maxWidth <= 0 || value.length <= maxWidth) {
5214
+ return value;
5215
+ }
5216
+ if (maxWidth <= 3) {
5217
+ return value.slice(0, maxWidth);
5218
+ }
5219
+ const available = maxWidth - 3;
5220
+ const head = Math.ceil(available / 2);
5221
+ const tail = Math.floor(available / 2);
5222
+ return `${value.slice(0, head)}...${value.slice(value.length - tail)}`;
5223
+ }
4649
5224
  function statusColor(status) {
4650
5225
  switch (status) {
4651
5226
  case "done":
@@ -4667,36 +5242,36 @@ function parseColumns(input2) {
4667
5242
  }
4668
5243
  function normalizeTaskColumns(value, ready) {
4669
5244
  if (!value?.trim()) {
4670
- return ready ? ["id", "title", "priority", "status", "assignee", "score"] : ["id", "title", "priority", "status", "assignee"];
5245
+ return ready ? ["id", "title", "priority", "status", "assignee", "track", "delivery", "score"] : ["id", "title", "priority", "status", "assignee", "track", "delivery"];
4671
5246
  }
4672
5247
  const raw = parseColumns(value);
4673
5248
  if (raw.length === 1 && raw[0] === "all") {
4674
- return ["id", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
5249
+ return ["id", "short", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
4675
5250
  }
4676
5251
  const normalized = raw.map((column) => {
4677
5252
  if (column === "p") return "priority";
4678
5253
  return column;
4679
5254
  });
4680
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
5255
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
4681
5256
  for (const column of normalized) {
4682
5257
  if (!valid.has(column)) {
4683
- throw new Error(`Invalid task column '${column}'. Expected id|title|status|priority|assignee|track|delivery|score|file|all.`);
5258
+ throw new Error(`Invalid task column '${column}'. Expected id|short|title|status|priority|assignee|track|delivery|score|file|all.`);
4684
5259
  }
4685
5260
  }
4686
5261
  return normalized;
4687
5262
  }
4688
5263
  function normalizeIdeaColumns(value) {
4689
5264
  if (!value?.trim()) {
4690
- return ["id", "title", "status"];
5265
+ return ["id", "short", "title", "status"];
4691
5266
  }
4692
5267
  const raw = parseColumns(value);
4693
5268
  if (raw.length === 1 && raw[0] === "all") {
4694
- return ["id", "title", "status", "file"];
5269
+ return ["id", "short", "title", "status", "file"];
4695
5270
  }
4696
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "file"]);
5271
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "file"]);
4697
5272
  for (const column of raw) {
4698
5273
  if (!valid.has(column)) {
4699
- throw new Error(`Invalid idea column '${column}'. Expected id|title|status|file|all.`);
5274
+ throw new Error(`Invalid idea column '${column}'. Expected id|short|title|status|file|all.`);
4700
5275
  }
4701
5276
  }
4702
5277
  return raw;
@@ -4833,6 +5408,8 @@ function sortTaskRows(rows, sortMode, readyOrder, track) {
4833
5408
  }
4834
5409
  function taskColumnHeader(column) {
4835
5410
  switch (column) {
5411
+ case "short":
5412
+ return "Short";
4836
5413
  case "priority":
4837
5414
  return "P";
4838
5415
  case "assignee":
@@ -4856,6 +5433,8 @@ function taskColumnHeader(column) {
4856
5433
  }
4857
5434
  function ideaColumnHeader(column) {
4858
5435
  switch (column) {
5436
+ case "short":
5437
+ return "Short";
4859
5438
  case "file":
4860
5439
  return "File";
4861
5440
  case "status":
@@ -4884,8 +5463,16 @@ function listTasks(options) {
4884
5463
  ensureCoopInitialized(root);
4885
5464
  const context = readWorkingContext(root, resolveCoopHome());
4886
5465
  const graph = load_graph6(coopDir(root));
4887
- const resolvedTrack = resolveContextValueWithSource(options.track, context.track);
4888
- const resolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
5466
+ const rawResolvedTrack = resolveContextValueWithSource(options.track, context.track);
5467
+ const rawResolvedDelivery = resolveContextValueWithSource(options.delivery, context.delivery);
5468
+ const resolvedTrack = {
5469
+ ...rawResolvedTrack,
5470
+ value: rawResolvedTrack.value ? resolveExistingTrackId(root, rawResolvedTrack.value) ?? rawResolvedTrack.value : void 0
5471
+ };
5472
+ const resolvedDelivery = {
5473
+ ...rawResolvedDelivery,
5474
+ value: rawResolvedDelivery.value ? resolveExistingDeliveryId(root, rawResolvedDelivery.value) ?? rawResolvedDelivery.value : void 0
5475
+ };
4889
5476
  const resolvedVersion = resolveContextValueWithSource(options.version, context.version);
4890
5477
  const assignee = options.mine ? defaultCoopAuthor(root) : options.assignee?.trim();
4891
5478
  const deliveryScope = resolvedDelivery.value ? new Set(graph.deliveries.get(resolvedDelivery.value)?.scope.include ?? []) : null;
@@ -4924,6 +5511,7 @@ function listTasks(options) {
4924
5511
  }).map(({ task, filePath }) => ({
4925
5512
  task,
4926
5513
  id: task.id,
5514
+ shortId: task.short_id ?? "-",
4927
5515
  title: task.title,
4928
5516
  status: task.status,
4929
5517
  priority: taskEffectivePriority(task, resolvedTrack.value),
@@ -4942,27 +5530,33 @@ function listTasks(options) {
4942
5530
  columns.map(taskColumnHeader),
4943
5531
  sorted.map(
4944
5532
  (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
- }
5533
+ const rawValue = (() => {
5534
+ switch (column) {
5535
+ case "short":
5536
+ return entry.shortId;
5537
+ case "title":
5538
+ return entry.title;
5539
+ case "status":
5540
+ return entry.status;
5541
+ case "priority":
5542
+ return entry.priority;
5543
+ case "assignee":
5544
+ return entry.assignee;
5545
+ case "track":
5546
+ return entry.track;
5547
+ case "delivery":
5548
+ return entry.delivery;
5549
+ case "score":
5550
+ return scoreMap.has(entry.id) ? scoreMap.get(entry.id).toFixed(1) : "-";
5551
+ case "file":
5552
+ return path15.relative(root, entry.filePath);
5553
+ case "id":
5554
+ default:
5555
+ return entry.id;
5556
+ }
5557
+ })();
5558
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
5559
+ return column === "status" ? statusColor(truncated) : truncated;
4966
5560
  })
4967
5561
  )
4968
5562
  )
@@ -4980,6 +5574,7 @@ function listIdeas(options) {
4980
5574
  return true;
4981
5575
  }).map(({ idea, filePath }) => ({
4982
5576
  id: idea.id,
5577
+ shortId: idea.short_id ?? "-",
4983
5578
  title: idea.title,
4984
5579
  status: idea.status,
4985
5580
  priority: "-",
@@ -5012,17 +5607,9 @@ function listIdeas(options) {
5012
5607
  columns.map(ideaColumnHeader),
5013
5608
  sorted.map(
5014
5609
  (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
- }
5610
+ const rawValue = column === "short" ? entry.shortId : column === "title" ? entry.title : column === "status" ? entry.status : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
5611
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
5612
+ return column === "status" ? statusColor(truncated) : truncated;
5026
5613
  })
5027
5614
  )
5028
5615
  )
@@ -5032,49 +5619,72 @@ Total ideas: ${sorted.length}`);
5032
5619
  }
5033
5620
  function listDeliveries() {
5034
5621
  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)
5622
+ const rows = loadDeliveryEntries(root).map(({ delivery, filePath }) => [
5623
+ truncateCell(delivery.id, 24),
5624
+ truncateCell(delivery.short_id ?? "-", 12),
5625
+ truncateCell(delivery.name, 30),
5626
+ statusColor(truncateCell(delivery.status, 12)),
5627
+ truncateCell(delivery.target_date ?? "-", 16),
5628
+ truncateMiddleCell(path15.relative(root, filePath), 30)
5041
5629
  ]);
5042
5630
  if (rows.length === 0) {
5043
5631
  console.log("No deliveries found.");
5044
5632
  return;
5045
5633
  }
5046
- console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
5634
+ console.log(formatTable(["ID", "Short", "Name", "Status", "Target", "File"], rows));
5635
+ }
5636
+ function listTracks() {
5637
+ const root = resolveRepoRoot();
5638
+ const rows = loadTrackEntries(root).map(({ track, filePath }) => [
5639
+ truncateCell(track.id, 24),
5640
+ truncateCell(track.short_id ?? "-", 12),
5641
+ truncateCell(track.name, 30),
5642
+ truncateCell((track.capacity_profiles ?? []).join(", ") || "-", 24),
5643
+ truncateCell(String(track.constraints?.max_concurrent_tasks ?? "-"), 8),
5644
+ truncateMiddleCell(path15.relative(root, filePath), 30)
5645
+ ]);
5646
+ if (rows.length === 0) {
5647
+ console.log("No tracks found.");
5648
+ return;
5649
+ }
5650
+ console.log(formatTable(["ID", "Short", "Name", "Profiles", "Max WIP", "File"], rows));
5047
5651
  }
5048
5652
  function registerListCommand(program) {
5049
5653
  const list = program.command("list").description("List COOP entities");
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) => {
5654
+ list.command("tasks").alias("task").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,short,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
5051
5655
  listTasks(options);
5052
5656
  });
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) => {
5657
+ list.command("ideas").alias("idea").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,short,title,status,file or all").action((options) => {
5054
5658
  listIdeas(options);
5055
5659
  });
5056
5660
  list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
5057
5661
  listAliasRows(pattern);
5058
5662
  });
5059
- list.command("deliveries").description("List deliveries").action(() => {
5663
+ list.command("tracks").alias("track").description("List tracks").action(() => {
5664
+ listTracks();
5665
+ });
5666
+ list.command("deliveries").alias("delivery").description("List deliveries").action(() => {
5060
5667
  listDeliveries();
5061
5668
  });
5062
5669
  }
5063
5670
 
5064
5671
  // src/utils/logger.ts
5065
5672
  import fs11 from "fs";
5673
+ import os2 from "os";
5066
5674
  import path16 from "path";
5067
5675
  function resolveWorkspaceRoot(start = process.cwd()) {
5068
5676
  let current = path16.resolve(start);
5677
+ const configuredCoopHome = path16.resolve(resolveCoopHome());
5678
+ const defaultCoopHome = path16.resolve(path16.join(os2.homedir(), ".coop"));
5069
5679
  while (true) {
5070
5680
  const gitDir = path16.join(current, ".git");
5071
5681
  const coopDir2 = coopWorkspaceDir(current);
5072
5682
  const workspaceConfig = path16.join(coopDir2, "config.yml");
5073
- const projectsDir = path16.join(coopDir2, "projects");
5074
5683
  if (fs11.existsSync(gitDir)) {
5075
5684
  return current;
5076
5685
  }
5077
- if (fs11.existsSync(workspaceConfig) || fs11.existsSync(projectsDir)) {
5686
+ const resolvedCoopDir = path16.resolve(coopDir2);
5687
+ if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
5078
5688
  return current;
5079
5689
  }
5080
5690
  const parent = path16.dirname(current);
@@ -5191,7 +5801,7 @@ function registerLogTimeCommand(program) {
5191
5801
  hours,
5192
5802
  options.note?.trim() || void 0
5193
5803
  );
5194
- writeTaskEntry(filePath, parsed, task);
5804
+ writeTaskEntry(root, filePath, parsed, task);
5195
5805
  console.log(`Logged ${hours}h ${options.kind} on ${task.id}`);
5196
5806
  });
5197
5807
  }
@@ -5201,7 +5811,7 @@ import fs12 from "fs";
5201
5811
  import path17 from "path";
5202
5812
  import { createInterface } from "readline/promises";
5203
5813
  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";
5814
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile5 } from "@kitsy/coop-core";
5205
5815
  var COOP_IGNORE_TEMPLATE2 = `.index/
5206
5816
  logs/
5207
5817
  tmp/
@@ -5368,7 +5978,7 @@ async function migrateWorkspaceLayout(root, options) {
5368
5978
  }
5369
5979
  const movedConfigPath = path17.join(projectRoot, "config.yml");
5370
5980
  if (fs12.existsSync(movedConfigPath)) {
5371
- const movedConfig = parseYamlFile2(movedConfigPath);
5981
+ const movedConfig = parseYamlFile5(movedConfigPath);
5372
5982
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
5373
5983
  nextProject.name = identity.projectName;
5374
5984
  nextProject.id = projectId;
@@ -5376,7 +5986,7 @@ async function migrateWorkspaceLayout(root, options) {
5376
5986
  const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
5377
5987
  nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
5378
5988
  nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
5379
- writeYamlFile4(movedConfigPath, {
5989
+ writeYamlFile5(movedConfigPath, {
5380
5990
  ...movedConfig,
5381
5991
  project: nextProject,
5382
5992
  hooks: nextHooks
@@ -5430,25 +6040,55 @@ function registerMigrateCommand(program) {
5430
6040
  }
5431
6041
 
5432
6042
  // src/commands/naming.ts
6043
+ var ENTITY_NAMES = ["task", "idea", "track", "delivery", "run"];
6044
+ function assertNamingEntity(value) {
6045
+ const entity = value?.trim().toLowerCase() || "task";
6046
+ if (!ENTITY_NAMES.includes(entity)) {
6047
+ throw new Error(`Invalid entity '${value}'. Expected ${ENTITY_NAMES.join("|")}.`);
6048
+ }
6049
+ return entity;
6050
+ }
6051
+ function readConfigRecord(root) {
6052
+ return readCoopConfig(root).raw;
6053
+ }
6054
+ function writeNamingConfig(root, update) {
6055
+ writeCoopConfig(root, update(readConfigRecord(root)));
6056
+ }
5433
6057
  function printNamingOverview() {
5434
6058
  const root = resolveRepoRoot();
5435
6059
  const config = readCoopConfig(root);
6060
+ const templates = namingTemplatesForRoot(root);
6061
+ const tokens = namingTokensForRoot(root);
5436
6062
  const sampleTitle = "Natural-language COOP command recommender";
5437
6063
  const sampleTaskTitle = "Implement billing payment contract review";
5438
6064
  console.log("COOP Naming");
5439
- console.log(`Current template: ${config.idNamingTemplate}`);
5440
- console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
5441
- console.log("Tokens:");
6065
+ console.log(`Legacy task template: ${config.idNamingTemplate}`);
6066
+ console.log(`Default task template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
6067
+ console.log("Per-entity templates:");
6068
+ for (const entity of ENTITY_NAMES) {
6069
+ console.log(`- ${entity}: ${templates[entity]}`);
6070
+ }
6071
+ console.log("Built-in tokens:");
5442
6072
  console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
5443
6073
  console.log(" <TITLE> semantic title token (defaults to TITLE16)");
5444
6074
  console.log(" <TITLE16> semantic title token capped to 16 chars");
5445
6075
  console.log(" <TITLE24> semantic title token capped to 24 chars");
5446
6076
  console.log(" <TRACK> task track");
6077
+ console.log(" <NAME> entity name/title");
6078
+ console.log(" <NAME_SLUG> lower-case slug of the entity name");
5447
6079
  console.log(" <SEQ> sequential number within the rendered pattern");
5448
6080
  console.log(" <USER> actor/user namespace");
5449
6081
  console.log(" <YYMMDD> short date token");
5450
6082
  console.log(" <RAND> random uniqueness token");
5451
6083
  console.log(" <PREFIX> entity prefix override");
6084
+ console.log("Custom tokens:");
6085
+ if (Object.keys(tokens).length === 0) {
6086
+ console.log("- none");
6087
+ } else {
6088
+ for (const [token, definition] of Object.entries(tokens)) {
6089
+ console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6090
+ }
6091
+ }
5452
6092
  console.log("Examples:");
5453
6093
  const tokenExamples = namingTokenExamples(sampleTitle);
5454
6094
  for (const [token, value] of Object.entries(tokenExamples)) {
@@ -5458,7 +6098,7 @@ function printNamingOverview() {
5458
6098
  entityType: "idea",
5459
6099
  title: sampleTitle
5460
6100
  }, root)}`);
5461
- console.log(`Preview (${config.idNamingTemplate}): ${previewNamingTemplate(config.idNamingTemplate, {
6101
+ console.log(`Preview (${templates.task}): ${previewNamingTemplate(templates.task, {
5462
6102
  entityType: "task",
5463
6103
  title: sampleTaskTitle,
5464
6104
  track: "mvp",
@@ -5467,35 +6107,187 @@ function printNamingOverview() {
5467
6107
  }, root)}`);
5468
6108
  console.log("Try:");
5469
6109
  console.log(` coop naming preview "${sampleTitle}"`);
5470
- console.log(' coop config id.naming "<TYPE>-<TITLE24>"');
5471
- console.log(' coop config id.naming "<TRACK>-<SEQ>"');
6110
+ console.log(" coop naming set task <TYPE>-<TITLE24>-<SEQ>");
6111
+ console.log(" coop naming token create proj");
6112
+ console.log(" coop naming token value add proj UX");
6113
+ }
6114
+ function setEntityNamingTemplate(root, entity, template) {
6115
+ const nextTemplate = template.trim();
6116
+ if (!nextTemplate) {
6117
+ throw new Error("Naming template must be non-empty.");
6118
+ }
6119
+ writeNamingConfig(root, (config) => {
6120
+ const next = { ...config };
6121
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6122
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
6123
+ namingRaw[entity] = nextTemplate;
6124
+ idRaw.naming = namingRaw;
6125
+ next.id = idRaw;
6126
+ return next;
6127
+ });
6128
+ }
6129
+ function ensureTokenRecord(config) {
6130
+ const next = { ...config };
6131
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6132
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? { ...idRaw.tokens } : {};
6133
+ idRaw.tokens = tokensRaw;
6134
+ next.id = idRaw;
6135
+ return { next, tokens: tokensRaw };
6136
+ }
6137
+ function listTokens() {
6138
+ const root = resolveRepoRoot();
6139
+ const tokens = namingTokensForRoot(root);
6140
+ if (Object.keys(tokens).length === 0) {
6141
+ console.log("No naming tokens defined.");
6142
+ return;
6143
+ }
6144
+ for (const [token, definition] of Object.entries(tokens)) {
6145
+ console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6146
+ }
5472
6147
  }
5473
6148
  function registerNamingCommand(program) {
5474
- const naming = program.command("naming").description("Explain COOP ID naming templates and preview examples");
6149
+ const naming = program.command("naming").description("Manage COOP naming templates, tokens, and previews");
5475
6150
  naming.action(() => {
5476
6151
  printNamingOverview();
5477
6152
  });
5478
- naming.command("preview").description("Preview the current or supplied naming template for a sample title").argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
6153
+ naming.command("preview").description("Preview the effective or supplied naming template for a sample title").allowUnknownOption().allowExcessArguments().argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
5479
6154
  (title, options) => {
5480
6155
  const root = resolveRepoRoot();
5481
- const config = readCoopConfig(root);
5482
- const entity = options.entity?.trim().toLowerCase() || "task";
5483
- const template = options.template?.trim() || config.idNamingTemplate;
6156
+ const entity = assertNamingEntity(options.entity);
6157
+ const templates = namingTemplatesForRoot(root);
6158
+ const dynamicFields = extractDynamicTokenFlags(
6159
+ ["naming", "preview"],
6160
+ ["template", "entity", "track", "status", "task-type"]
6161
+ );
6162
+ const tokens = namingTokensForRoot(root);
6163
+ for (const key of Object.keys(dynamicFields)) {
6164
+ if (!tokens[key]) {
6165
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
6166
+ }
6167
+ }
5484
6168
  console.log(
5485
6169
  previewNamingTemplate(
5486
- template,
6170
+ options.template?.trim() || templates[entity],
5487
6171
  {
5488
6172
  entityType: entity,
5489
6173
  title,
6174
+ name: title,
5490
6175
  track: options.track,
5491
6176
  status: options.status,
5492
- taskType: options.taskType
6177
+ taskType: options.taskType,
6178
+ fields: dynamicFields
5493
6179
  },
5494
6180
  root
5495
6181
  )
5496
6182
  );
5497
6183
  }
5498
6184
  );
6185
+ naming.command("set").description("Set the naming template for one entity type").argument("<entity>", "Entity: task|idea|track|delivery|run").argument("<template>", "Naming template").action((entity, template) => {
6186
+ const root = resolveRepoRoot();
6187
+ setEntityNamingTemplate(root, assertNamingEntity(entity), template);
6188
+ console.log(`${assertNamingEntity(entity)}=${namingTemplatesForRoot(root)[assertNamingEntity(entity)]}`);
6189
+ });
6190
+ const token = naming.command("token").description("Manage custom naming tokens");
6191
+ token.command("list").description("List naming tokens").action(() => {
6192
+ listTokens();
6193
+ });
6194
+ token.command("create").description("Create a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6195
+ const root = resolveRepoRoot();
6196
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6197
+ writeNamingConfig(root, (config) => {
6198
+ const { next, tokens } = ensureTokenRecord(config);
6199
+ if (tokens[normalizedToken]) {
6200
+ throw new Error(`Naming token '${normalizedToken}' already exists.`);
6201
+ }
6202
+ tokens[normalizedToken] = { values: [] };
6203
+ return next;
6204
+ });
6205
+ console.log(`Created naming token: ${normalizedToken}`);
6206
+ });
6207
+ token.command("rename").description("Rename a custom naming token").argument("<token>", "Existing token name").argument("<next>", "New token name").action((tokenName, nextToken) => {
6208
+ const root = resolveRepoRoot();
6209
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6210
+ const normalizedNext = normalizeNamingTokenName(nextToken);
6211
+ const tokens = namingTokensForRoot(root);
6212
+ if (!tokens[normalizedToken]) {
6213
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6214
+ }
6215
+ if (tokens[normalizedNext]) {
6216
+ throw new Error(`Naming token '${normalizedNext}' already exists.`);
6217
+ }
6218
+ writeNamingConfig(root, (config) => {
6219
+ const { next, tokens: tokensRaw } = ensureTokenRecord(config);
6220
+ tokensRaw[normalizedNext] = tokensRaw[normalizedToken];
6221
+ delete tokensRaw[normalizedToken];
6222
+ return next;
6223
+ });
6224
+ console.log(`Renamed naming token: ${normalizedToken} -> ${normalizedNext}`);
6225
+ });
6226
+ token.command("delete").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6227
+ const root = resolveRepoRoot();
6228
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6229
+ writeNamingConfig(root, (config) => {
6230
+ const { next, tokens } = ensureTokenRecord(config);
6231
+ delete tokens[normalizedToken];
6232
+ return next;
6233
+ });
6234
+ console.log(`Deleted naming token: ${normalizedToken}`);
6235
+ });
6236
+ const tokenValue = token.command("value").description("Manage allowed values for a naming token");
6237
+ tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
6238
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6239
+ const tokens = namingTokensForRoot(resolveRepoRoot());
6240
+ const values = tokens[normalizedToken]?.values;
6241
+ if (!values) {
6242
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6243
+ }
6244
+ if (values.length === 0) {
6245
+ console.log("(no values)");
6246
+ return;
6247
+ }
6248
+ for (const value of values) {
6249
+ console.log(value);
6250
+ }
6251
+ });
6252
+ tokenValue.command("add").description("Add an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6253
+ const root = resolveRepoRoot();
6254
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6255
+ const normalizedValue = normalizeNamingTokenValue(value);
6256
+ writeNamingConfig(root, (config) => {
6257
+ const { next, tokens } = ensureTokenRecord(config);
6258
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6259
+ if (!tokenRecord) {
6260
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6261
+ }
6262
+ const values = Array.isArray(tokenRecord.values) ? Array.from(
6263
+ new Set(
6264
+ tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
6265
+ )
6266
+ ) : [];
6267
+ values.push(normalizedValue);
6268
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6269
+ tokens[normalizedToken] = tokenRecord;
6270
+ return next;
6271
+ });
6272
+ console.log(`Added naming token value: ${normalizedToken}=${normalizedValue}`);
6273
+ });
6274
+ tokenValue.command("remove").description("Remove an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6275
+ const root = resolveRepoRoot();
6276
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6277
+ const normalizedValue = normalizeNamingTokenValue(value);
6278
+ writeNamingConfig(root, (config) => {
6279
+ const { next, tokens } = ensureTokenRecord(config);
6280
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6281
+ if (!tokenRecord) {
6282
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6283
+ }
6284
+ const values = Array.isArray(tokenRecord.values) ? tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry)).filter((entry) => entry !== normalizedValue) : [];
6285
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6286
+ tokens[normalizedToken] = tokenRecord;
6287
+ return next;
6288
+ });
6289
+ console.log(`Removed naming token value: ${normalizedToken}=${normalizedValue}`);
6290
+ });
5499
6291
  }
5500
6292
 
5501
6293
  // src/commands/plan.ts
@@ -5763,7 +6555,7 @@ function registerPromoteCommand(program) {
5763
6555
  track: resolvedTrack.value,
5764
6556
  version: resolvedVersion.value
5765
6557
  });
5766
- writeTaskEntry(filePath, parsed, task);
6558
+ writeTaskEntry(root, filePath, parsed, task);
5767
6559
  console.log(`Promoted ${task.id}`);
5768
6560
  });
5769
6561
  }
@@ -5816,7 +6608,13 @@ id_prefixes:
5816
6608
  run: "RUN"
5817
6609
 
5818
6610
  id:
5819
- naming: ${JSON.stringify(namingTemplate)}
6611
+ naming:
6612
+ task: ${JSON.stringify(namingTemplate)}
6613
+ idea: ${JSON.stringify(namingTemplate)}
6614
+ track: "<NAME_SLUG>"
6615
+ delivery: "<NAME_SLUG>"
6616
+ run: "<TYPE>-<YYMMDD>-<RAND>"
6617
+ tokens: {}
5820
6618
  seq_padding: 0
5821
6619
 
5822
6620
  defaults:
@@ -6236,7 +7034,7 @@ function registerRunCommand(program) {
6236
7034
  }
6237
7035
 
6238
7036
  // src/commands/search.ts
6239
- import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile3, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
7037
+ import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile4, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
6240
7038
  function haystackForTask(task) {
6241
7039
  return [
6242
7040
  task.id,
@@ -6315,7 +7113,7 @@ ${parsed.body}`, query)) continue;
6315
7113
  }
6316
7114
  if (options.kind === "all" || options.kind === "delivery") {
6317
7115
  for (const filePath of listDeliveryFiles(root)) {
6318
- const parsed = parseDeliveryFile3(filePath);
7116
+ const parsed = parseDeliveryFile4(filePath);
6319
7117
  if (options.status && parsed.delivery.status !== options.status) continue;
6320
7118
  if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
6321
7119
  rows.push({
@@ -6659,6 +7457,7 @@ function showTask(taskId, options = {}) {
6659
7457
  if (options.compact) {
6660
7458
  const compactLines = [
6661
7459
  `Task: ${task.id}`,
7460
+ `Short ID: ${task.short_id ?? "-"}`,
6662
7461
  `Title: ${task.title}`,
6663
7462
  `Status: ${task.status}`,
6664
7463
  `Priority: ${task.priority ?? "-"}`,
@@ -6676,6 +7475,7 @@ function showTask(taskId, options = {}) {
6676
7475
  }
6677
7476
  const lines = [
6678
7477
  `Task: ${task.id}`,
7478
+ `Short ID: ${task.short_id ?? "-"}`,
6679
7479
  `Title: ${task.title}`,
6680
7480
  `Status: ${task.status}`,
6681
7481
  `Type: ${task.type}`,
@@ -6749,6 +7549,7 @@ function showIdea(ideaId, options = {}) {
6749
7549
  console.log(
6750
7550
  [
6751
7551
  `Idea: ${idea.id}`,
7552
+ `Short ID: ${idea.short_id ?? "-"}`,
6752
7553
  `Title: ${idea.title}`,
6753
7554
  `Status: ${idea.status}`,
6754
7555
  `Tags: ${stringify(idea.tags)}`,
@@ -6760,6 +7561,7 @@ function showIdea(ideaId, options = {}) {
6760
7561
  }
6761
7562
  const lines = [
6762
7563
  `Idea: ${idea.id}`,
7564
+ `Short ID: ${idea.short_id ?? "-"}`,
6763
7565
  `Title: ${idea.title}`,
6764
7566
  `Status: ${idea.status}`,
6765
7567
  `Author: ${idea.author}`,
@@ -6782,6 +7584,7 @@ function showDelivery(ref, options = {}) {
6782
7584
  console.log(
6783
7585
  [
6784
7586
  `Delivery: ${delivery.id}`,
7587
+ `Short ID: ${delivery.short_id ?? "-"}`,
6785
7588
  `Name: ${delivery.name}`,
6786
7589
  `Status: ${delivery.status}`,
6787
7590
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6793,6 +7596,7 @@ function showDelivery(ref, options = {}) {
6793
7596
  }
6794
7597
  const lines = [
6795
7598
  `Delivery: ${delivery.id}`,
7599
+ `Short ID: ${delivery.short_id ?? "-"}`,
6796
7600
  `Name: ${delivery.name}`,
6797
7601
  `Status: ${delivery.status}`,
6798
7602
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6808,6 +7612,28 @@ function showDelivery(ref, options = {}) {
6808
7612
  ];
6809
7613
  console.log(lines.join("\n"));
6810
7614
  }
7615
+ function showTrack(ref, options = {}) {
7616
+ const root = resolveRepoRoot();
7617
+ const resolvedId = resolveExistingTrackId(root, ref);
7618
+ if (!resolvedId) {
7619
+ throw new Error(`Track '${ref}' not found.`);
7620
+ }
7621
+ const entry = loadTrackEntries(root).find((candidate) => candidate.track.id === resolvedId);
7622
+ if (!entry) {
7623
+ throw new Error(`Track '${ref}' not found.`);
7624
+ }
7625
+ const track = entry.track;
7626
+ const base = [
7627
+ `Track: ${track.id}`,
7628
+ `Short ID: ${track.short_id ?? "-"}`,
7629
+ `Name: ${track.name}`,
7630
+ `Profiles: ${(track.capacity_profiles ?? []).join(", ") || "-"}`,
7631
+ `Max WIP: ${track.constraints?.max_concurrent_tasks ?? "-"}`,
7632
+ `Allowed Types: ${(track.constraints?.allowed_types ?? []).join(", ") || "-"}`,
7633
+ `File: ${path22.relative(root, entry.filePath)}`
7634
+ ];
7635
+ console.log((options.compact ? base.filter((line) => !line.startsWith("Allowed Types")) : base).join("\n"));
7636
+ }
6811
7637
  function showByReference(ref, options = {}) {
6812
7638
  const root = resolveRepoRoot();
6813
7639
  try {
@@ -6819,7 +7645,11 @@ function showByReference(ref, options = {}) {
6819
7645
  showIdea(ref, options);
6820
7646
  return;
6821
7647
  } catch {
6822
- showDelivery(ref, options);
7648
+ try {
7649
+ showDelivery(ref, options);
7650
+ } catch {
7651
+ showTrack(ref, options);
7652
+ }
6823
7653
  }
6824
7654
  }
6825
7655
  function registerShowCommand(program) {
@@ -6835,6 +7665,9 @@ function registerShowCommand(program) {
6835
7665
  show.command("idea").description("Show idea details").argument("<id>", "Idea ID").option("--compact", "Show a smaller summary view").action((id, options) => {
6836
7666
  showIdea(id, options);
6837
7667
  });
7668
+ show.command("track").description("Show track details").argument("<id>", "Track id, short id, or unique name").option("--compact", "Show a smaller summary view").action((id, options) => {
7669
+ showTrack(id, options);
7670
+ });
6838
7671
  show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").option("--compact", "Show a smaller summary view").action((id, options) => {
6839
7672
  showDelivery(id, options);
6840
7673
  });
@@ -7012,7 +7845,7 @@ function maybePromote(root, taskId, options) {
7012
7845
  track: options.track ?? context.track,
7013
7846
  version: options.version ?? context.version
7014
7847
  });
7015
- writeTaskEntry(filePath, parsed, promoted);
7848
+ writeTaskEntry(root, filePath, parsed, promoted);
7016
7849
  console.log(`Promoted ${promoted.id}`);
7017
7850
  }
7018
7851
  function printResolvedSelectionContext(root, options) {
@@ -7224,7 +8057,7 @@ import { IdeaStatus as IdeaStatus3, TaskPriority as TaskPriority3, TaskStatus as
7224
8057
  function collect(value, previous = []) {
7225
8058
  return [...previous, value];
7226
8059
  }
7227
- function unique2(items) {
8060
+ function unique3(items) {
7228
8061
  return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
7229
8062
  }
7230
8063
  function removeValues(source, values) {
@@ -7234,7 +8067,7 @@ function removeValues(source, values) {
7234
8067
  return next.length > 0 ? next : void 0;
7235
8068
  }
7236
8069
  function addValues(source, values) {
7237
- const next = unique2([...source ?? [], ...values ?? []]);
8070
+ const next = unique3([...source ?? [], ...values ?? []]);
7238
8071
  return next.length > 0 ? next : void 0;
7239
8072
  }
7240
8073
  function loadBody(options) {
@@ -7316,13 +8149,13 @@ function updateTask(id, options) {
7316
8149
  tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
7317
8150
  };
7318
8151
  next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
7319
- validateTaskForWrite(next, filePath);
8152
+ next = validateTaskForWrite(root, next, filePath);
7320
8153
  const nextBody = loadBody(options) ?? parsed.body;
7321
8154
  if (options.dryRun) {
7322
8155
  console.log(renderTaskPreview(next, nextBody).trimEnd());
7323
8156
  return;
7324
8157
  }
7325
- writeTaskEntry(filePath, parsed, next, nextBody);
8158
+ writeTaskEntry(root, filePath, parsed, next, nextBody);
7326
8159
  console.log(`Updated ${next.id}`);
7327
8160
  }
7328
8161
  function updateIdea(id, options) {
@@ -7392,11 +8225,19 @@ function registerUseCommand(program) {
7392
8225
  });
7393
8226
  use.command("track").description("Set the default working track").argument("<id>", "Track id").action((id) => {
7394
8227
  const root = resolveRepoRoot();
7395
- printContext(updateWorkingContext(root, resolveCoopHome(), { track: id }));
8228
+ const trackId = resolveExistingTrackId(root, id);
8229
+ if (!trackId) {
8230
+ throw new Error(`Unknown track '${id}'. Create it first with \`coop create track --id ${id} --name "${id}"\` or use \`unassigned\`.`);
8231
+ }
8232
+ printContext(updateWorkingContext(root, resolveCoopHome(), { track: trackId }));
7396
8233
  });
7397
8234
  use.command("delivery").description("Set the default working delivery").argument("<id>", "Delivery id").action((id) => {
7398
8235
  const root = resolveRepoRoot();
7399
- printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: id }));
8236
+ const deliveryId = resolveExistingDeliveryId(root, id);
8237
+ if (!deliveryId) {
8238
+ throw new Error(`Unknown delivery '${id}'. Create it first with \`coop create delivery --id ${id} --name "${id}"\`.`);
8239
+ }
8240
+ printContext(updateWorkingContext(root, resolveCoopHome(), { delivery: deliveryId }));
7400
8241
  });
7401
8242
  use.command("version").description("Set the default working version").argument("<id>", "Version label").action((id) => {
7402
8243
  const root = resolveRepoRoot();
@@ -7409,6 +8250,10 @@ function registerUseCommand(program) {
7409
8250
  const root = resolveRepoRoot();
7410
8251
  printContext(clearWorkingContext(root, resolveCoopHome(), scope));
7411
8252
  });
8253
+ use.command("reset").description("Clear all working-context values").action(() => {
8254
+ const root = resolveRepoRoot();
8255
+ printContext(clearWorkingContext(root, resolveCoopHome(), "all"));
8256
+ });
7412
8257
  }
7413
8258
 
7414
8259
  // src/commands/view.ts
@@ -7749,7 +8594,7 @@ function registerWebhookCommand(program) {
7749
8594
 
7750
8595
  // src/merge-driver/merge-driver.ts
7751
8596
  import fs21 from "fs";
7752
- import os2 from "os";
8597
+ import os3 from "os";
7753
8598
  import path25 from "path";
7754
8599
  import { spawnSync as spawnSync5 } from "child_process";
7755
8600
  import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
@@ -7840,7 +8685,7 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
7840
8685
  const ours = parseTaskDocument(oursRaw, oursPath);
7841
8686
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
7842
8687
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
7843
- const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
8688
+ const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
7844
8689
  try {
7845
8690
  const ancestorBody = path25.join(tempDir, "ancestor.md");
7846
8691
  const oursBody = path25.join(tempDir, "ours.md");
@@ -7887,6 +8732,9 @@ function renderBasicHelp() {
7887
8732
  "Day-to-day commands:",
7888
8733
  "- `coop current`: show working context, active work, and the next ready task",
7889
8734
  "- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
8735
+ "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
8736
+ "- `coop naming`: inspect per-entity ID rules and naming tokens",
8737
+ '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item',
7890
8738
  "- `coop next task` or `coop pick task`: choose work from COOP",
7891
8739
  "- `coop show <id>`: inspect a task, idea, or delivery",
7892
8740
  "- `coop list tasks --track <id>`: browse scoped work",
@@ -7934,6 +8782,7 @@ Common day-to-day commands:
7934
8782
  coop current
7935
8783
  coop next task
7936
8784
  coop show <id>
8785
+ coop naming
7937
8786
  `);
7938
8787
  registerInitCommand(program);
7939
8788
  registerCreateCommand(program);