@kitsy/coop 2.2.2 → 2.2.4

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 +900 -134
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -32,7 +32,33 @@ 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
+ "TRACK",
48
+ "STATUS",
49
+ "TASK_TYPE",
50
+ "PREFIX",
51
+ "USER",
52
+ "YYMMDD",
53
+ "RAND",
54
+ "SEQ",
55
+ "NAME",
56
+ "NAME_SLUG"
57
+ ]);
58
+ function isBuiltInNamingToken(token) {
59
+ const upper = token.toUpperCase();
60
+ return BUILT_IN_NAMING_TOKENS.has(upper) || /^TITLE\d{1,2}$/.test(upper);
61
+ }
36
62
  var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
37
63
  "A",
38
64
  "AN",
@@ -60,8 +86,12 @@ function resolveCoopHome() {
60
86
  }
61
87
  function resolveRepoRoot(start = process.cwd()) {
62
88
  let current = path.resolve(start);
89
+ const configuredCoopHome = path.resolve(resolveCoopHome());
90
+ const defaultCoopHome = path.resolve(path.join(os.homedir(), ".coop"));
63
91
  while (true) {
64
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".coop"))) {
92
+ const coopDir2 = path.join(current, ".coop");
93
+ const isGlobalCoopHome = path.resolve(coopDir2) === configuredCoopHome || path.resolve(coopDir2) === defaultCoopHome;
94
+ if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(coopDir2) && !isGlobalCoopHome) {
65
95
  return current;
66
96
  }
67
97
  const parent = path.dirname(current);
@@ -126,6 +156,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
126
156
  taskPrefix: "PM",
127
157
  indexDataFormat: "yaml",
128
158
  idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
159
+ idNamingTemplates: { ...DEFAULT_NAMING_TEMPLATES },
160
+ idTokens: {},
129
161
  idSeqPadding: 0,
130
162
  artifactsDir: DEFAULT_ARTIFACTS_DIR,
131
163
  projectName: repoName || "COOP Workspace",
@@ -147,9 +179,10 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
147
179
  const indexRaw = typeof config.index === "object" && config.index !== null ? config.index : {};
148
180
  const indexDataRaw = indexRaw.data;
149
181
  const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
182
+ const idNamingTemplates = readNamingTemplates(config);
183
+ const idNamingTemplate = idNamingTemplates.task;
184
+ const idTokens = readNamingTokens(config);
150
185
  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
186
  const idSeqPaddingRaw = idRaw.seq_padding;
154
187
  const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
155
188
  const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
@@ -159,6 +192,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
159
192
  taskPrefix,
160
193
  indexDataFormat,
161
194
  idNamingTemplate,
195
+ idNamingTemplates,
196
+ idTokens,
162
197
  idSeqPadding,
163
198
  artifactsDir,
164
199
  projectName: projectName || "COOP Workspace",
@@ -176,6 +211,59 @@ function sanitizeIdentityPart(value, fallback) {
176
211
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
177
212
  return normalized || fallback;
178
213
  }
214
+ function slugifyLower(value, fallback = "item") {
215
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
216
+ return normalized || fallback;
217
+ }
218
+ function normalizeNamingTokenName(value) {
219
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
220
+ if (!normalized) {
221
+ throw new Error("Naming token names must contain letters, numbers, or underscores.");
222
+ }
223
+ return normalized;
224
+ }
225
+ function normalizeNamingTokenValue(value) {
226
+ const normalized = value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
227
+ if (!normalized) {
228
+ throw new Error("Naming token values must contain letters or numbers.");
229
+ }
230
+ return normalized;
231
+ }
232
+ function readNamingTemplates(rawConfig) {
233
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
234
+ const namingRaw = idRaw.naming;
235
+ if (typeof namingRaw === "string" && namingRaw.trim().length > 0) {
236
+ return {
237
+ ...DEFAULT_NAMING_TEMPLATES,
238
+ task: namingRaw.trim(),
239
+ idea: namingRaw.trim()
240
+ };
241
+ }
242
+ const namingRecord = typeof namingRaw === "object" && namingRaw !== null ? namingRaw : {};
243
+ return {
244
+ task: typeof namingRecord.task === "string" && namingRecord.task.trim().length > 0 ? namingRecord.task.trim() : DEFAULT_NAMING_TEMPLATES.task,
245
+ idea: typeof namingRecord.idea === "string" && namingRecord.idea.trim().length > 0 ? namingRecord.idea.trim() : DEFAULT_NAMING_TEMPLATES.idea,
246
+ track: typeof namingRecord.track === "string" && namingRecord.track.trim().length > 0 ? namingRecord.track.trim() : DEFAULT_NAMING_TEMPLATES.track,
247
+ delivery: typeof namingRecord.delivery === "string" && namingRecord.delivery.trim().length > 0 ? namingRecord.delivery.trim() : DEFAULT_NAMING_TEMPLATES.delivery,
248
+ run: typeof namingRecord.run === "string" && namingRecord.run.trim().length > 0 ? namingRecord.run.trim() : DEFAULT_NAMING_TEMPLATES.run
249
+ };
250
+ }
251
+ function readNamingTokens(rawConfig) {
252
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
253
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? idRaw.tokens : {};
254
+ const tokens = {};
255
+ for (const [rawName, rawValue] of Object.entries(tokensRaw)) {
256
+ const name = normalizeNamingTokenName(rawName);
257
+ const valuesRecord = typeof rawValue === "object" && rawValue !== null ? rawValue : {};
258
+ const values = Array.isArray(valuesRecord.values) ? Array.from(
259
+ new Set(
260
+ valuesRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
261
+ )
262
+ ).sort((a, b) => a.localeCompare(b)) : [];
263
+ tokens[name] = { values };
264
+ }
265
+ return tokens;
266
+ }
179
267
  function repoDisplayName(root) {
180
268
  const base = path.basename(path.resolve(root)).trim();
181
269
  return base || "COOP Workspace";
@@ -278,6 +366,19 @@ function sanitizeTemplateValue(input2, fallback = "X") {
278
366
  const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
279
367
  return normalized || fallback;
280
368
  }
369
+ function normalizeEntityIdValue(entityType, input2, fallback = "COOP-ID") {
370
+ if (entityType === "track" || entityType === "delivery") {
371
+ return slugifyLower(input2, slugifyLower(fallback, "item"));
372
+ }
373
+ return sanitizeTemplateValue(input2, fallback);
374
+ }
375
+ function extractTemplateTokens(template) {
376
+ return Array.from(
377
+ new Set(
378
+ Array.from(template.matchAll(/<([^>]+)>/g)).map((match) => match[1]?.trim().toUpperCase()).filter((token) => Boolean(token))
379
+ )
380
+ );
381
+ }
281
382
  function sanitizeSemanticWord(input2) {
282
383
  return input2.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
283
384
  }
@@ -301,7 +402,7 @@ function semanticTitleToken(input2, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
301
402
  const segments = [];
302
403
  let used = 0;
303
404
  for (const word of words) {
304
- const remaining = safeMaxLength - used - (segments.length > 0 ? 1 : 0);
405
+ const remaining = safeMaxLength - used;
305
406
  if (remaining < 2) {
306
407
  break;
307
408
  }
@@ -310,7 +411,7 @@ function semanticTitleToken(input2, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
310
411
  continue;
311
412
  }
312
413
  segments.push(segment);
313
- used += segment.length + (segments.length > 1 ? 1 : 0);
414
+ used += segment.length;
314
415
  }
315
416
  if (segments.length > 0) {
316
417
  return segments.join("-");
@@ -332,8 +433,8 @@ function namingTokenExamples(title = "Natural-language COOP command recommender"
332
433
  };
333
434
  }
334
435
  function sequenceForPattern(existingIds, prefix, suffix) {
335
- const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
336
- const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
436
+ const escapedPrefix = prefix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
437
+ const escapedSuffix = suffix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
337
438
  const regex = new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`);
338
439
  let max = 0;
339
440
  for (const id of existingIds) {
@@ -359,6 +460,7 @@ function buildIdContext(root, config, context) {
359
460
  const track = context.track?.trim() || "";
360
461
  const status = context.status?.trim() || "";
361
462
  const title = context.title?.trim() || "";
463
+ const name = context.name?.trim() || context.title?.trim() || "";
362
464
  const prefix = context.prefix?.trim() || (context.entityType === "idea" ? config.ideaPrefix : config.taskPrefix);
363
465
  const actor = inferActor(root);
364
466
  const map = {
@@ -366,6 +468,7 @@ function buildIdContext(root, config, context) {
366
468
  ENTITY: sanitizeTemplateValue(entityType, entityType),
367
469
  USER: normalizeIdPart(actor, "USER", 16),
368
470
  YYMMDD: shortDateToken(now),
471
+ TITLE_SOURCE: title,
369
472
  TITLE: semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
370
473
  TITLE16: semanticTitleToken(title, 16),
371
474
  TITLE24: semanticTitleToken(title, 24),
@@ -373,13 +476,26 @@ function buildIdContext(root, config, context) {
373
476
  STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
374
477
  TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
375
478
  PREFIX: sanitizeTemplateValue(prefix, "COOP"),
479
+ NAME: name || "item",
480
+ NAME_SLUG: slugifyLower(name || "item"),
376
481
  RAND: randomToken()
377
482
  };
378
483
  for (const [key, value] of Object.entries(fields)) {
379
484
  if (!value || !value.trim()) continue;
380
- const upper = key.toUpperCase();
485
+ const tokenName = normalizeNamingTokenName(key);
486
+ const upper = tokenName.toUpperCase();
381
487
  if (Object.prototype.hasOwnProperty.call(map, upper)) continue;
382
- map[upper] = sanitizeTemplateValue(value);
488
+ const tokenConfig = config.idTokens[tokenName];
489
+ if (!tokenConfig) {
490
+ continue;
491
+ }
492
+ const normalizedValue = normalizeNamingTokenValue(value);
493
+ if (tokenConfig.values.length > 0 && !tokenConfig.values.includes(normalizedValue)) {
494
+ throw new Error(
495
+ `Invalid value '${value}' for naming token '${tokenName}'. Allowed values: ${tokenConfig.values.join(", ")}.`
496
+ );
497
+ }
498
+ map[upper] = normalizedValue;
383
499
  }
384
500
  return map;
385
501
  }
@@ -392,58 +508,152 @@ function replaceTemplateToken(token, contextMap) {
392
508
  if (upper === "TYPE" || upper === "ENTITY") return contextMap.TYPE;
393
509
  if (upper === "TITLE") return contextMap.TITLE;
394
510
  if (/^TITLE\d+$/.test(upper)) {
395
- return contextMap[upper] || contextMap.TITLE;
511
+ const parsed = Number(upper.slice("TITLE".length));
512
+ if (Number.isInteger(parsed) && parsed > 0) {
513
+ return semanticTitleToken(contextMap.TITLE_SOURCE ?? "", parsed);
514
+ }
515
+ return contextMap.TITLE;
396
516
  }
397
517
  if (upper === "TRACK") return contextMap.TRACK;
398
518
  if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
399
519
  if (upper === "STATUS") return contextMap.STATUS;
400
520
  if (upper === "PREFIX") return contextMap.PREFIX;
521
+ if (upper === "NAME") return contextMap.NAME;
522
+ if (upper === "NAME_SLUG") return contextMap.NAME_SLUG;
401
523
  const dynamic = contextMap[upper];
402
524
  if (dynamic) return dynamic;
403
525
  return sanitizeTemplateValue(upper);
404
526
  }
405
- function renderNamingTemplate(template, contextMap) {
527
+ function renderNamingTemplate(template, contextMap, entityType) {
406
528
  const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
407
529
  const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
408
- return sanitizeTemplateValue(replaced, "COOP-ID");
530
+ return normalizeEntityIdValue(entityType, replaced, "COOP-ID");
409
531
  }
410
532
  function defaultCoopAuthor(root) {
411
533
  const actor = inferActor(root);
412
534
  return actor.trim() || "unknown";
413
535
  }
536
+ function normalizeEntityId(entityType, value) {
537
+ const trimmed = value.trim();
538
+ if (!trimmed) {
539
+ throw new Error(`Invalid ${entityType} id. Provide a non-empty value.`);
540
+ }
541
+ return normalizeEntityIdValue(entityType, trimmed, entityType);
542
+ }
543
+ function namingTemplatesForRoot(root) {
544
+ return readCoopConfig(root).idNamingTemplates;
545
+ }
546
+ function namingTokensForRoot(root) {
547
+ return readCoopConfig(root).idTokens;
548
+ }
549
+ function requiredCustomNamingTokens(root, entityType) {
550
+ const config = readCoopConfig(root);
551
+ const template = config.idNamingTemplates[entityType];
552
+ return extractTemplateTokens(template).filter((token) => !isBuiltInNamingToken(token)).map((token) => token.toLowerCase());
553
+ }
554
+ function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
555
+ const config = readCoopConfig(root);
556
+ const digest = crypto.createHash("sha256").update(`${config.projectId}:${entityType}:${primaryId}`).digest("hex").toLowerCase();
557
+ const existing = new Set(existingShortIds.map((value) => value.toLowerCase()));
558
+ for (let width = 12; width <= digest.length; width += 2) {
559
+ const candidate = digest.slice(0, width);
560
+ if (!existing.has(candidate)) {
561
+ return candidate;
562
+ }
563
+ }
564
+ throw new Error(`Unable to generate a unique short id for ${entityType} '${primaryId}'.`);
565
+ }
566
+ function extractDynamicTokenFlags(commandPath, knownValueOptions, knownBooleanOptions = [], argv = process.argv.slice(2)) {
567
+ const start = argv.findIndex((_, index) => commandPath.every((segment, offset) => argv[index + offset] === segment));
568
+ if (start < 0) {
569
+ return {};
570
+ }
571
+ const result = {};
572
+ const valueOptions = new Set(knownValueOptions);
573
+ const booleanOptions = new Set(knownBooleanOptions);
574
+ for (let index = start + commandPath.length; index < argv.length; index += 1) {
575
+ const token = argv[index] ?? "";
576
+ if (!token.startsWith("--")) {
577
+ continue;
578
+ }
579
+ const [rawName, inlineValue] = token.slice(2).split("=", 2);
580
+ const name = rawName.trim();
581
+ if (!name) {
582
+ continue;
583
+ }
584
+ if (valueOptions.has(name)) {
585
+ if (inlineValue === void 0) {
586
+ index += 1;
587
+ }
588
+ continue;
589
+ }
590
+ if (booleanOptions.has(name) || name.startsWith("no-")) {
591
+ continue;
592
+ }
593
+ const value = inlineValue ?? argv[index + 1];
594
+ if (!value || value.startsWith("-")) {
595
+ throw new Error(`Dynamic naming flag '--${name}' requires a value.`);
596
+ }
597
+ result[name] = value;
598
+ if (inlineValue === void 0) {
599
+ index += 1;
600
+ }
601
+ }
602
+ return result;
603
+ }
414
604
  function previewNamingTemplate(template, context, root = process.cwd()) {
415
605
  const config = readCoopConfig(root);
606
+ const usedTemplate = template.trim().length > 0 ? template : config.idNamingTemplates[context.entityType];
607
+ const referencedTokens = extractTemplateTokens(usedTemplate);
608
+ for (const token of referencedTokens) {
609
+ if (!isBuiltInNamingToken(token) && !config.idTokens[token.toLowerCase()]) {
610
+ throw new Error(`Naming template references unknown token <${token}>.`);
611
+ }
612
+ }
416
613
  const contextMap = buildIdContext(root, config, context);
417
- const rendered = renderNamingTemplate(template, {
614
+ const rendered = renderNamingTemplate(usedTemplate, {
418
615
  ...contextMap,
419
616
  RAND: "AB12CD34",
420
617
  YYMMDD: "260320",
421
618
  USER: "PKVSI"
422
- });
619
+ }, context.entityType);
423
620
  return rendered.replace(SEQ_MARKER, "1");
424
621
  }
425
622
  function generateConfiguredId(root, existingIds, context) {
426
623
  const config = readCoopConfig(root);
624
+ const template = config.idNamingTemplates[context.entityType];
625
+ const referencedTokens = extractTemplateTokens(template);
626
+ for (const token of referencedTokens) {
627
+ if (isBuiltInNamingToken(token)) {
628
+ continue;
629
+ }
630
+ if (!config.idTokens[token.toLowerCase()]) {
631
+ throw new Error(`Naming template references unknown token <${token}>.`);
632
+ }
633
+ if (!context.fields || !Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === token.toLowerCase())) {
634
+ throw new Error(`Naming template requires token <${token}>. Pass --${token.toLowerCase()} <value>.`);
635
+ }
636
+ }
427
637
  const contextMap = buildIdContext(root, config, context);
428
638
  const existing = existingIds.map((id) => id.toUpperCase());
429
639
  for (let attempt = 0; attempt < 32; attempt += 1) {
430
- const rendered = renderNamingTemplate(config.idNamingTemplate, {
640
+ const rendered = renderNamingTemplate(template, {
431
641
  ...contextMap,
432
642
  RAND: randomToken()
433
- });
643
+ }, context.entityType);
434
644
  const seqMarker = SEQ_MARKER;
435
- const upperRendered = rendered.toUpperCase();
436
- if (!upperRendered.includes(seqMarker)) {
437
- if (!existing.includes(upperRendered)) {
438
- return upperRendered;
645
+ const normalizedRendered = context.entityType === "track" || context.entityType === "delivery" ? rendered : rendered.toUpperCase();
646
+ if (!normalizedRendered.includes(seqMarker)) {
647
+ if (!existing.includes(normalizedRendered.toUpperCase())) {
648
+ return normalizedRendered;
439
649
  }
440
650
  continue;
441
651
  }
442
- const [prefix, suffix] = upperRendered.split(seqMarker);
652
+ const [prefix, suffix] = normalizedRendered.split(seqMarker);
443
653
  const nextSeq = sequenceForPattern(existing, prefix ?? "", suffix ?? "");
444
654
  const seq = padSequence(nextSeq, config.idSeqPadding);
445
655
  const candidate = `${prefix ?? ""}${seq}${suffix ?? ""}`.replace(/-+/g, "-").replace(/^-+|-+$/g, "");
446
- if (!existing.includes(candidate)) {
656
+ if (!existing.includes(candidate.toUpperCase())) {
447
657
  return candidate;
448
658
  }
449
659
  }
@@ -551,32 +761,58 @@ function rebuildAliasIndex(root) {
551
761
  ensureCoopInitialized(root);
552
762
  const items = {};
553
763
  const aliases = {};
764
+ const shortIds = {};
554
765
  const ids = /* @__PURE__ */ new Set();
555
766
  const taskFiles = listTaskFiles(root);
767
+ const usedTaskShortIds = taskFiles.map((filePath) => parseTaskFile2(filePath)).map((parsed) => parsed.task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
556
768
  for (const filePath of taskFiles) {
557
769
  const parsed = parseTaskFile2(filePath);
558
770
  const id = parsed.task.id.toUpperCase();
559
771
  const source = path2.relative(root, filePath);
772
+ const shortId = parsed.task.short_id?.trim() ? parsed.task.short_id.trim().toLowerCase() : generateStableShortId(root, "task", parsed.task.id, usedTaskShortIds);
773
+ if (!parsed.task.short_id?.trim()) {
774
+ usedTaskShortIds.push(shortId);
775
+ const nextRaw = { ...parsed.raw, short_id: shortId };
776
+ const nextTask = { ...parsed.task, short_id: shortId };
777
+ writeTask(nextTask, { body: parsed.body, raw: nextRaw, filePath });
778
+ }
560
779
  ids.add(id);
561
780
  items[id] = {
562
781
  type: "task",
563
782
  aliases: parseAliases(parsed.raw, source),
564
- file: toPosixPath(path2.relative(root, filePath))
783
+ file: toPosixPath(path2.relative(root, filePath)),
784
+ short_id: shortId
565
785
  };
566
786
  }
567
787
  const ideaFiles = listIdeaFiles(root);
788
+ const usedIdeaShortIds = ideaFiles.map((filePath) => parseIdeaFile(filePath)).map((parsed) => parsed.idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
568
789
  for (const filePath of ideaFiles) {
569
790
  const parsed = parseIdeaFile(filePath);
570
791
  const id = parsed.idea.id.toUpperCase();
571
792
  const source = path2.relative(root, filePath);
793
+ const shortId = parsed.idea.short_id?.trim() ? parsed.idea.short_id.trim().toLowerCase() : generateStableShortId(root, "idea", parsed.idea.id, usedIdeaShortIds);
794
+ if (!parsed.idea.short_id?.trim()) {
795
+ usedIdeaShortIds.push(shortId);
796
+ const nextRaw = { ...parsed.raw, short_id: shortId };
797
+ const output2 = stringifyFrontmatter(nextRaw, parsed.body);
798
+ fs2.writeFileSync(filePath, output2, "utf8");
799
+ }
572
800
  ids.add(id);
573
801
  items[id] = {
574
802
  type: "idea",
575
803
  aliases: parseAliases(parsed.raw, source),
576
- file: toPosixPath(path2.relative(root, filePath))
804
+ file: toPosixPath(path2.relative(root, filePath)),
805
+ short_id: shortId
577
806
  };
578
807
  }
579
808
  for (const [id, item] of Object.entries(items)) {
809
+ if (item.short_id) {
810
+ const existingShort = shortIds[item.short_id];
811
+ if (existingShort && existingShort.id !== id) {
812
+ throw new Error(`Short id '${item.short_id}' is already mapped to '${existingShort.id}'.`);
813
+ }
814
+ shortIds[item.short_id] = { id, type: item.type, file: item.file, short_id: item.short_id };
815
+ }
580
816
  for (const alias of item.aliases) {
581
817
  if (ids.has(alias)) {
582
818
  throw new Error(`Alias '${alias}' conflicts with existing item id '${alias}'.`);
@@ -592,6 +828,7 @@ function rebuildAliasIndex(root) {
592
828
  version: 1,
593
829
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
594
830
  aliases,
831
+ short_ids: shortIds,
595
832
  items
596
833
  };
597
834
  writeIndexFile(root, data);
@@ -604,7 +841,7 @@ function loadAliasIndex(root) {
604
841
  return rebuildAliasIndex(root);
605
842
  }
606
843
  const parsed = readIndexFile(root, aliasIndexPath);
607
- if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items) {
844
+ if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items || !parsed.short_ids) {
608
845
  return rebuildAliasIndex(root);
609
846
  }
610
847
  return parsed;
@@ -619,6 +856,27 @@ function resolveReference(root, idOrAlias, expectedType) {
619
856
  }
620
857
  return { id: idCandidate, type: item.type, file: item.file };
621
858
  }
859
+ const shortCandidate = idOrAlias.trim().toLowerCase();
860
+ const shortMatch = index.short_ids[shortCandidate];
861
+ if (shortMatch) {
862
+ if (expectedType && shortMatch.type !== expectedType) {
863
+ throw new Error(`'${idOrAlias}' resolves to ${shortMatch.type} '${shortMatch.id}', expected ${expectedType}.`);
864
+ }
865
+ return shortMatch;
866
+ }
867
+ if (shortCandidate.length >= 6) {
868
+ const prefixMatches = Object.entries(index.short_ids).filter(([shortId]) => shortId.startsWith(shortCandidate)).map(([, target]) => target);
869
+ if (prefixMatches.length === 1) {
870
+ const match2 = prefixMatches[0];
871
+ if (expectedType && match2.type !== expectedType) {
872
+ throw new Error(`'${idOrAlias}' resolves to ${match2.type} '${match2.id}', expected ${expectedType}.`);
873
+ }
874
+ return match2;
875
+ }
876
+ if (prefixMatches.length > 1) {
877
+ throw new Error(`Ambiguous short id '${idOrAlias}'. Candidates: ${prefixMatches.map((entry) => entry.id).join(", ")}.`);
878
+ }
879
+ }
622
880
  const alias = normalizeAlias(idOrAlias);
623
881
  const match = index.aliases[alias];
624
882
  if (!match) {
@@ -725,17 +983,28 @@ function removeAliases(root, idOrAlias, values) {
725
983
  }
726
984
 
727
985
  // src/utils/table.ts
986
+ var ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
987
+ function visibleLength(value) {
988
+ return value.replace(ANSI_PATTERN, "").length;
989
+ }
990
+ function padEndAnsi(value, width) {
991
+ const visible = visibleLength(value);
992
+ if (visible >= width) {
993
+ return value;
994
+ }
995
+ return `${value}${" ".repeat(width - visible)}`;
996
+ }
728
997
  function formatTable(headers, rows) {
729
998
  const table = [headers, ...rows];
730
999
  const widths = [];
731
1000
  for (let col = 0; col < headers.length; col += 1) {
732
- let width = headers[col].length;
1001
+ let width = visibleLength(headers[col] ?? "");
733
1002
  for (const row of table) {
734
- width = Math.max(width, (row[col] ?? "").length);
1003
+ width = Math.max(width, visibleLength(row[col] ?? ""));
735
1004
  }
736
1005
  widths.push(width + 2);
737
1006
  }
738
- return table.map((row) => row.map((cell, index) => (cell ?? "").padEnd(widths[index])).join("").trimEnd()).join("\n");
1007
+ return table.map((row) => row.map((cell, index) => padEndAnsi(cell ?? "", widths[index])).join("").trimEnd()).join("\n");
739
1008
  }
740
1009
 
741
1010
  // src/commands/alias.ts
@@ -780,7 +1049,7 @@ function registerAliasCommand(program) {
780
1049
  const suffix = result.added.length > 0 ? result.added.join(", ") : "no new aliases";
781
1050
  console.log(`Updated ${result.target.type} ${result.target.id}: ${suffix}`);
782
1051
  });
783
- alias.command("rm").description("Remove aliases from an item").argument("<idOrAlias>", "Target item id or alias").argument("<aliases...>", "Aliases to remove").action((idOrAlias, aliases) => {
1052
+ alias.command("rm").alias("remove").description("Remove aliases from an item").argument("<idOrAlias>", "Target item id or alias").argument("<aliases...>", "Aliases to remove").action((idOrAlias, aliases) => {
784
1053
  const root = resolveRepoRoot();
785
1054
  const result = removeAliases(root, idOrAlias, aliases);
786
1055
  const suffix = result.removed.length > 0 ? result.removed.join(", ") : "no aliases removed";
@@ -1398,38 +1667,101 @@ import {
1398
1667
  stringifyFrontmatter as stringifyFrontmatter2,
1399
1668
  validateStructural as validateStructural2,
1400
1669
  validateSemantic,
1670
+ writeYamlFile as writeYamlFile3,
1401
1671
  writeTask as writeTask3
1402
1672
  } from "@kitsy/coop-core";
1403
- function existingTrackMap(root) {
1404
- const map = /* @__PURE__ */ new Map();
1405
- for (const filePath of listTrackFiles(root)) {
1406
- const track = parseYamlFile2(filePath);
1407
- if (track.id?.trim()) {
1408
- map.set(track.id.trim().toLowerCase(), track.id.trim());
1673
+ function writeTrackFile(filePath, track) {
1674
+ writeYamlFile3(filePath, track);
1675
+ }
1676
+ function writeDeliveryFile(filePath, delivery, raw, body) {
1677
+ const payload = { ...raw, ...delivery };
1678
+ if (body.trim().length > 0) {
1679
+ fs3.writeFileSync(filePath, stringifyFrontmatter2(payload, body), "utf8");
1680
+ return;
1681
+ }
1682
+ writeYamlFile3(filePath, payload);
1683
+ }
1684
+ function loadTrackEntries(root) {
1685
+ const rawEntries = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile2(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id));
1686
+ const used = rawEntries.map((entry) => entry.track.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1687
+ let mutated = false;
1688
+ for (const entry of rawEntries) {
1689
+ if (entry.track.short_id?.trim()) {
1690
+ continue;
1409
1691
  }
1692
+ entry.track.short_id = generateStableShortId(root, "track", entry.track.id, used);
1693
+ used.push(entry.track.short_id);
1694
+ writeTrackFile(entry.filePath, entry.track);
1695
+ mutated = true;
1410
1696
  }
1411
- return map;
1697
+ return mutated ? rawEntries.map((entry) => ({ track: parseYamlFile2(entry.filePath), filePath: entry.filePath })) : rawEntries;
1412
1698
  }
1413
- function existingDeliveryMap(root) {
1414
- const map = /* @__PURE__ */ new Map();
1415
- for (const filePath of listDeliveryFiles(root)) {
1416
- const delivery = parseDeliveryFile(filePath).delivery;
1417
- if (delivery.id?.trim()) {
1418
- map.set(delivery.id.trim().toLowerCase(), delivery.id.trim());
1699
+ function loadDeliveryEntries(root) {
1700
+ const rawEntries = listDeliveryFiles(root).map((filePath) => {
1701
+ const parsed = parseDeliveryFile(filePath);
1702
+ return { delivery: parsed.delivery, filePath, body: parsed.body, raw: parsed.raw };
1703
+ }).sort((a, b) => a.delivery.id.localeCompare(b.delivery.id));
1704
+ const used = rawEntries.map((entry) => entry.delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1705
+ let mutated = false;
1706
+ for (const entry of rawEntries) {
1707
+ if (entry.delivery.short_id?.trim()) {
1708
+ continue;
1419
1709
  }
1710
+ entry.delivery.short_id = generateStableShortId(root, "delivery", entry.delivery.id, used);
1711
+ used.push(entry.delivery.short_id);
1712
+ writeDeliveryFile(entry.filePath, entry.delivery, entry.raw, entry.body);
1713
+ mutated = true;
1420
1714
  }
1421
- return map;
1715
+ return mutated ? rawEntries.map((entry) => {
1716
+ const parsed = parseDeliveryFile(entry.filePath);
1717
+ return { delivery: parsed.delivery, filePath: entry.filePath, body: parsed.body, raw: parsed.raw };
1718
+ }) : rawEntries;
1719
+ }
1720
+ function resolveUniqueShortId(entries, value) {
1721
+ const normalized = value.trim().toLowerCase();
1722
+ if (!normalized) return void 0;
1723
+ const exact = entries.find((entry) => entry.short_id?.toLowerCase() === normalized);
1724
+ if (exact) return exact;
1725
+ if (normalized.length < 6) return void 0;
1726
+ const matches = entries.filter((entry) => entry.short_id?.toLowerCase().startsWith(normalized));
1727
+ if (matches.length === 1) {
1728
+ return matches[0];
1729
+ }
1730
+ if (matches.length > 1) {
1731
+ throw new Error(`Ambiguous short id '${value}'. Candidates: ${matches.map((entry) => entry.id).join(", ")}.`);
1732
+ }
1733
+ return void 0;
1422
1734
  }
1423
1735
  function resolveExistingTrackId(root, trackId) {
1424
1736
  const normalized = trackId.trim().toLowerCase();
1425
1737
  if (!normalized) return void 0;
1426
1738
  if (normalized === "unassigned") return "unassigned";
1427
- return existingTrackMap(root).get(normalized);
1739
+ const entries = loadTrackEntries(root);
1740
+ const byId = entries.find((entry) => entry.track.id.trim().toLowerCase() === normalized);
1741
+ if (byId) return byId.track.id.trim();
1742
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.track), normalized);
1743
+ if (byShort) return byShort.id.trim();
1744
+ const byName = entries.filter((entry) => entry.track.name.trim().toLowerCase() === normalized);
1745
+ if (byName.length === 1) return byName[0].track.id.trim();
1746
+ if (byName.length > 1) {
1747
+ throw new Error(`Multiple tracks match '${trackId}'. Use the track id or short id.`);
1748
+ }
1749
+ return void 0;
1428
1750
  }
1429
1751
  function resolveExistingDeliveryId(root, deliveryId) {
1430
1752
  const normalized = deliveryId.trim().toLowerCase();
1431
1753
  if (!normalized) return void 0;
1432
- return existingDeliveryMap(root).get(normalized);
1754
+ const entries = loadDeliveryEntries(root);
1755
+ const byId = entries.find((entry) => entry.delivery.id.trim().toLowerCase() === normalized);
1756
+ if (byId) return byId.delivery.id.trim();
1757
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), normalized);
1758
+ if (byShort) return byShort.id.trim();
1759
+ const byName = entries.filter((entry) => entry.delivery.name.trim().toLowerCase() === normalized);
1760
+ if (byName.length === 1) return byName[0].delivery.id.trim();
1761
+ if (byName.length > 1) {
1762
+ throw new Error(`Multiple deliveries match '${deliveryId}'. Use the delivery id or short id.`);
1763
+ }
1764
+ return void 0;
1433
1765
  }
1434
1766
  function assertExistingTrackId(root, trackId) {
1435
1767
  const resolved = resolveExistingTrackId(root, trackId);
@@ -1480,11 +1812,27 @@ function resolveIdeaFile(root, idOrAlias) {
1480
1812
  }
1481
1813
  function loadTaskEntry(root, idOrAlias) {
1482
1814
  const filePath = resolveTaskFile(root, idOrAlias);
1483
- return { filePath, parsed: parseTaskFile4(filePath) };
1815
+ const parsed = parseTaskFile4(filePath);
1816
+ if (!parsed.task.short_id?.trim()) {
1817
+ const shortId = generateStableShortId(root, "task", parsed.task.id);
1818
+ const nextTask = { ...parsed.task, short_id: shortId };
1819
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1820
+ writeTask3(nextTask, { body: parsed.body, raw: nextRaw, filePath });
1821
+ return { filePath, parsed: parseTaskFile4(filePath) };
1822
+ }
1823
+ return { filePath, parsed };
1484
1824
  }
1485
1825
  function loadIdeaEntry(root, idOrAlias) {
1486
1826
  const filePath = resolveIdeaFile(root, idOrAlias);
1487
- return { filePath, parsed: parseIdeaFile2(filePath) };
1827
+ const parsed = parseIdeaFile2(filePath);
1828
+ if (!parsed.idea.short_id?.trim()) {
1829
+ const shortId = generateStableShortId(root, "idea", parsed.idea.id);
1830
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1831
+ const output2 = stringifyFrontmatter2(nextRaw, parsed.body);
1832
+ fs3.writeFileSync(filePath, output2, "utf8");
1833
+ return { filePath, parsed: parseIdeaFile2(filePath) };
1834
+ }
1835
+ return { filePath, parsed };
1488
1836
  }
1489
1837
  function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
1490
1838
  const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
@@ -1566,18 +1914,18 @@ function taskEffectivePriority(task, track) {
1566
1914
  return effective_priority(task, track);
1567
1915
  }
1568
1916
  function resolveDeliveryEntry(root, ref) {
1569
- const files = listDeliveryFiles(root);
1570
1917
  const target = ref.trim().toLowerCase();
1571
- const entries = files.map((filePath) => {
1572
- const parsed = parseDeliveryFile(filePath);
1573
- return { filePath, delivery: parsed.delivery, body: parsed.body };
1574
- });
1918
+ const entries = loadDeliveryEntries(root);
1575
1919
  const direct = entries.find((entry) => entry.delivery.id.toLowerCase() === target);
1576
1920
  if (direct) return direct;
1921
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), target);
1922
+ if (byShort) {
1923
+ return entries.find((entry) => entry.delivery.id === byShort.id);
1924
+ }
1577
1925
  const byName = entries.filter((entry) => entry.delivery.name.toLowerCase() === target);
1578
1926
  if (byName.length === 1) return byName[0];
1579
1927
  if (byName.length > 1) {
1580
- throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id.`);
1928
+ throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id or short id.`);
1581
1929
  }
1582
1930
  throw new Error(`Delivery '${ref}' not found.`);
1583
1931
  }
@@ -1716,6 +2064,7 @@ function selectTopReadyTask(root = resolveRepoRoot(), options = {}) {
1716
2064
  function formatSelectedTask(entry, selection = {}) {
1717
2065
  const lines = [
1718
2066
  `Selected task: ${entry.task.id}`,
2067
+ `Short ID: ${entry.task.short_id ?? "-"}`,
1719
2068
  `Title: ${entry.task.title}`,
1720
2069
  `Priority: ${selection.track && entry.task.priority_context?.[selection.track] ? `${entry.task.priority ?? "-"} -> ${entry.task.priority_context[selection.track]}` : entry.task.priority ?? "-"}`,
1721
2070
  `Track: ${entry.task.track ?? "-"}`,
@@ -2038,7 +2387,9 @@ function writeIdNamingValue(root, value) {
2038
2387
  const config = readCoopConfig(root).raw;
2039
2388
  const next = { ...config };
2040
2389
  const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
2041
- idRaw.naming = nextValue;
2390
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
2391
+ namingRaw.task = nextValue;
2392
+ idRaw.naming = namingRaw;
2042
2393
  next.id = idRaw;
2043
2394
  writeCoopConfig(root, next);
2044
2395
  }
@@ -2209,8 +2560,10 @@ import {
2209
2560
  TaskType as TaskType2,
2210
2561
  check_permission as check_permission2,
2211
2562
  load_auth_config as load_auth_config2,
2563
+ parseDeliveryFile as parseDeliveryFile2,
2212
2564
  parseTaskFile as parseTaskFile7,
2213
- writeYamlFile as writeYamlFile3,
2565
+ parseYamlFile as parseYamlFile3,
2566
+ writeYamlFile as writeYamlFile4,
2214
2567
  stringifyFrontmatter as stringifyFrontmatter4,
2215
2568
  writeTask as writeTask6
2216
2569
  } from "@kitsy/coop-core";
@@ -2713,6 +3066,65 @@ function plusDaysIso(days) {
2713
3066
  function unique2(values) {
2714
3067
  return Array.from(new Set(values));
2715
3068
  }
3069
+ function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
3070
+ const normalizedName = candidateName.trim().toLowerCase();
3071
+ if (!normalizedName) {
3072
+ return;
3073
+ }
3074
+ const conflict = entries.find(
3075
+ (entry) => entry.id.trim().toLowerCase() !== candidateId.trim().toLowerCase() && entry.name.trim().toLowerCase() === normalizedName
3076
+ );
3077
+ if (!conflict) {
3078
+ return;
3079
+ }
3080
+ throw new Error(
3081
+ `${kind === "track" ? "Track" : "Delivery"} name '${candidateName}' conflicts with existing ${kind} '${conflict.id}' (${conflict.name}). Names are matched case-insensitively.`
3082
+ );
3083
+ }
3084
+ function taskShortIds(root) {
3085
+ return listTaskFiles(root).map((filePath) => parseTaskFile7(filePath).task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3086
+ }
3087
+ function ideaShortIds(root) {
3088
+ return listIdeaFiles(root).map((filePath) => parseIdeaFile3(filePath).idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3089
+ }
3090
+ function trackShortIds(root) {
3091
+ return listTrackFiles(root).map((filePath) => parseYamlFile3(filePath).short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3092
+ }
3093
+ function deliveryShortIds(root) {
3094
+ return listDeliveryFiles(root).map((filePath) => parseDeliveryFile2(filePath).delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3095
+ }
3096
+ function assertKnownDynamicFields(root, fields) {
3097
+ const tokens = namingTokensForRoot(root);
3098
+ for (const key of Object.keys(fields)) {
3099
+ if (!tokens[key]) {
3100
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
3101
+ }
3102
+ }
3103
+ }
3104
+ async function collectRequiredNamingFields(root, entityType, fields) {
3105
+ const next = { ...fields };
3106
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3107
+ return next;
3108
+ }
3109
+ const tokens = namingTokensForRoot(root);
3110
+ for (const token of requiredCustomNamingTokens(root, entityType)) {
3111
+ const existing = next[token]?.trim();
3112
+ if (existing) {
3113
+ continue;
3114
+ }
3115
+ const definition = tokens[token];
3116
+ if (definition && definition.values.length > 0) {
3117
+ const choice = await select(
3118
+ `Naming token ${token}`,
3119
+ definition.values.map((value) => ({ label: value, value }))
3120
+ );
3121
+ next[token] = choice;
3122
+ continue;
3123
+ }
3124
+ next[token] = await ask(`Naming token ${token}`);
3125
+ }
3126
+ return next;
3127
+ }
2716
3128
  function resolveIdeaFile2(root, idOrAlias) {
2717
3129
  const target = resolveReference(root, idOrAlias, "idea");
2718
3130
  return path8.join(root, ...target.file.split("/"));
@@ -2741,10 +3153,34 @@ function makeTaskDraft(input2) {
2741
3153
  }
2742
3154
  function registerCreateCommand(program) {
2743
3155
  const create = program.command("create").description("Create COOP entities");
2744
- 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) => {
3156
+ 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) => {
2745
3157
  const root = resolveRepoRoot();
2746
3158
  const coop = ensureCoopInitialized(root);
2747
3159
  const interactive = Boolean(options.interactive);
3160
+ let dynamicFields = extractDynamicTokenFlags(
3161
+ ["create", "task"],
3162
+ [
3163
+ "id",
3164
+ "from",
3165
+ "title",
3166
+ "type",
3167
+ "status",
3168
+ "track",
3169
+ "delivery",
3170
+ "priority",
3171
+ "body",
3172
+ "acceptance",
3173
+ "tests-required",
3174
+ "authority-ref",
3175
+ "derived-ref",
3176
+ "from-file",
3177
+ "stdin",
3178
+ "interactive",
3179
+ "ai"
3180
+ ]
3181
+ );
3182
+ assertKnownDynamicFields(root, dynamicFields);
3183
+ dynamicFields = await collectRequiredNamingFields(root, "task", dynamicFields);
2748
3184
  if (options.fromFile?.trim() || options.stdin) {
2749
3185
  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) {
2750
3186
  throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
@@ -2854,23 +3290,28 @@ function registerCreateCommand(program) {
2854
3290
  }
2855
3291
  const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
2856
3292
  const createdIds = [];
3293
+ const existingShortIds = taskShortIds(root);
3294
+ const createdShortIds = [];
2857
3295
  for (let index = 0; index < drafts.length; index += 1) {
2858
3296
  const draft = drafts[index];
2859
3297
  if (!draft) continue;
2860
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, [...existingIds, ...createdIds], {
3298
+ const id = (options.id?.trim() ? normalizeEntityId("task", options.id) : void 0) || generateConfiguredId(root, [...existingIds, ...createdIds], {
2861
3299
  entityType: "task",
2862
3300
  title: draft.title,
2863
3301
  taskType: draft.type,
2864
3302
  track: draft.track,
2865
3303
  status: draft.status,
3304
+ name: draft.title,
2866
3305
  fields: {
2867
3306
  track: draft.track,
2868
3307
  type: draft.type,
2869
- feature: draft.track || draft.type
3308
+ feature: draft.track || draft.type,
3309
+ ...dynamicFields
2870
3310
  }
2871
3311
  });
2872
3312
  const task = {
2873
3313
  id,
3314
+ short_id: generateStableShortId(root, "task", id, [...existingShortIds, ...createdShortIds]),
2874
3315
  title: draft.title,
2875
3316
  type: draft.type,
2876
3317
  status: draft.status,
@@ -2895,6 +3336,9 @@ function registerCreateCommand(program) {
2895
3336
  filePath
2896
3337
  });
2897
3338
  createdIds.push(id);
3339
+ if (normalizedTask.short_id) {
3340
+ createdShortIds.push(normalizedTask.short_id);
3341
+ }
2898
3342
  console.log(`Created task: ${path8.relative(root, filePath)}`);
2899
3343
  }
2900
3344
  if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
@@ -2908,10 +3352,16 @@ function registerCreateCommand(program) {
2908
3352
  console.log(`Linked ${createdIds.length} task(s) to idea: ${sourceIdeaParsed.idea.id}`);
2909
3353
  }
2910
3354
  });
2911
- 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) => {
3355
+ 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) => {
2912
3356
  const root = resolveRepoRoot();
2913
3357
  const coop = ensureCoopInitialized(root);
2914
3358
  const interactive = Boolean(options.interactive);
3359
+ let dynamicFields = extractDynamicTokenFlags(
3360
+ ["create", "idea"],
3361
+ ["id", "title", "author", "source", "status", "tags", "body", "from-file", "stdin", "interactive"]
3362
+ );
3363
+ assertKnownDynamicFields(root, dynamicFields);
3364
+ dynamicFields = await collectRequiredNamingFields(root, "idea", dynamicFields);
2915
3365
  if (options.fromFile?.trim() || options.stdin) {
2916
3366
  if (options.id || options.title || titleArg || options.author || options.source || options.status || options.tags || options.body) {
2917
3367
  throw new Error("Cannot combine --from-file/--stdin with direct idea field flags. Use one input mode.");
@@ -2933,17 +3383,21 @@ function registerCreateCommand(program) {
2933
3383
  const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
2934
3384
  const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
2935
3385
  const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
2936
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3386
+ const id = (options.id?.trim() ? normalizeEntityId("idea", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2937
3387
  entityType: "idea",
2938
3388
  title,
3389
+ name: title,
2939
3390
  status,
2940
3391
  fields: {
2941
3392
  source,
2942
- author
3393
+ author,
3394
+ ...dynamicFields
2943
3395
  }
2944
3396
  });
3397
+ const shortId = generateStableShortId(root, "idea", id, ideaShortIds(root));
2945
3398
  const frontmatter = {
2946
3399
  id,
3400
+ short_id: shortId,
2947
3401
  title,
2948
3402
  created: todayIsoDate(),
2949
3403
  aliases: [],
@@ -2960,10 +3414,16 @@ function registerCreateCommand(program) {
2960
3414
  fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
2961
3415
  console.log(`Created idea: ${path8.relative(root, filePath)}`);
2962
3416
  });
2963
- 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) => {
3417
+ 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) => {
2964
3418
  const root = resolveRepoRoot();
2965
3419
  const coop = ensureCoopInitialized(root);
2966
3420
  const interactive = Boolean(options.interactive);
3421
+ let dynamicFields = extractDynamicTokenFlags(
3422
+ ["create", "track"],
3423
+ ["id", "name", "profiles", "max-wip", "allowed-types", "interactive"]
3424
+ );
3425
+ assertKnownDynamicFields(root, dynamicFields);
3426
+ dynamicFields = await collectRequiredNamingFields(root, "track", dynamicFields);
2967
3427
  const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
2968
3428
  if (!name) throw new Error("Track name is required.");
2969
3429
  const capacityProfiles = unique2(
@@ -2985,19 +3445,21 @@ function registerCreateCommand(program) {
2985
3445
  }
2986
3446
  }
2987
3447
  const existingIds = listTrackFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml)$/i, ""));
2988
- const config = readCoopConfig(root);
2989
- const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
2990
- const prefix = typeof idPrefixesRaw.track === "string" ? idPrefixesRaw.track : "TRK";
2991
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3448
+ const id = (options.id?.trim() ? normalizeEntityId("track", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2992
3449
  entityType: "track",
2993
- prefix,
2994
3450
  title: name,
3451
+ name,
2995
3452
  fields: {
2996
- name
3453
+ name,
3454
+ ...dynamicFields
2997
3455
  }
2998
3456
  });
3457
+ const existingTracks = listTrackFiles(root).map((filePath2) => parseYamlFile3(filePath2));
3458
+ assertNoCaseInsensitiveNameConflict("track", existingTracks, id, name);
3459
+ const shortId = generateStableShortId(root, "track", id, trackShortIds(root));
2999
3460
  const payload = {
3000
3461
  id,
3462
+ short_id: shortId,
3001
3463
  name,
3002
3464
  capacity_profiles: capacityProfiles,
3003
3465
  constraints: {
@@ -3006,13 +3468,33 @@ function registerCreateCommand(program) {
3006
3468
  }
3007
3469
  };
3008
3470
  const filePath = path8.join(coop, "tracks", `${id}.yml`);
3009
- writeYamlFile3(filePath, payload);
3471
+ writeYamlFile4(filePath, payload);
3010
3472
  console.log(`Created track: ${path8.relative(root, filePath)}`);
3011
3473
  });
3012
- 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) => {
3474
+ 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) => {
3013
3475
  const root = resolveRepoRoot();
3014
3476
  const coop = ensureCoopInitialized(root);
3015
3477
  const interactive = Boolean(options.interactive);
3478
+ let dynamicFields = extractDynamicTokenFlags(
3479
+ ["create", "delivery"],
3480
+ [
3481
+ "id",
3482
+ "name",
3483
+ "status",
3484
+ "commit",
3485
+ "target-date",
3486
+ "budget-hours",
3487
+ "budget-cost",
3488
+ "profiles",
3489
+ "scope",
3490
+ "exclude",
3491
+ "user",
3492
+ "force",
3493
+ "interactive"
3494
+ ]
3495
+ );
3496
+ assertKnownDynamicFields(root, dynamicFields);
3497
+ dynamicFields = await collectRequiredNamingFields(root, "delivery", dynamicFields);
3016
3498
  const user = options.user?.trim() || defaultCoopAuthor(root);
3017
3499
  const config = readCoopConfig(root);
3018
3500
  const auth = load_auth_config2(config.raw);
@@ -3088,21 +3570,22 @@ function registerCreateCommand(program) {
3088
3570
  throw new Error(`Scope exclude task '${id2}' does not exist.`);
3089
3571
  }
3090
3572
  }
3091
- const existingIds = listDeliveryFiles(root).map(
3092
- (filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, "")
3093
- );
3094
- const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
3095
- const prefix = typeof idPrefixesRaw.delivery === "string" ? idPrefixesRaw.delivery : "DEL";
3096
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3573
+ const existingIds = listDeliveryFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, ""));
3574
+ const id = (options.id?.trim() ? normalizeEntityId("delivery", options.id) : void 0) || generateConfiguredId(root, existingIds, {
3097
3575
  entityType: "delivery",
3098
- prefix,
3099
3576
  title: name,
3577
+ name,
3100
3578
  fields: {
3101
- status
3579
+ status,
3580
+ ...dynamicFields
3102
3581
  }
3103
3582
  });
3583
+ const existingDeliveries = listDeliveryFiles(root).map((filePath2) => parseDeliveryFile2(filePath2).delivery);
3584
+ assertNoCaseInsensitiveNameConflict("delivery", existingDeliveries, id, name);
3585
+ const shortId = generateStableShortId(root, "delivery", id, deliveryShortIds(root));
3104
3586
  const payload = {
3105
3587
  id,
3588
+ short_id: shortId,
3106
3589
  name,
3107
3590
  status,
3108
3591
  target_date: targetDate,
@@ -3119,7 +3602,7 @@ function registerCreateCommand(program) {
3119
3602
  }
3120
3603
  };
3121
3604
  const filePath = path8.join(coop, "deliveries", `${id}.yml`);
3122
- writeYamlFile3(filePath, payload);
3605
+ writeYamlFile4(filePath, payload);
3123
3606
  console.log(`Created delivery: ${path8.relative(root, filePath)}`);
3124
3607
  });
3125
3608
  }
@@ -3411,6 +3894,7 @@ var catalog = {
3411
3894
  "Use `coop project show` first to confirm the active workspace and project.",
3412
3895
  "Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
3413
3896
  "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`.",
3897
+ "Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
3414
3898
  "Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
3415
3899
  "Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
3416
3900
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
@@ -3468,11 +3952,18 @@ var catalog = {
3468
3952
  { usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
3469
3953
  { usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
3470
3954
  { usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
3955
+ { usage: "coop use reset", purpose: "Clear the user-local working track, delivery, and version defaults." },
3471
3956
  { usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
3472
3957
  { usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
3473
3958
  { usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
3474
- { usage: "coop naming", purpose: "Explain the current naming template, tokens, and examples." },
3475
- { usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview a semantic ID before creating an item." }
3959
+ { usage: "coop naming", purpose: "Explain the effective per-entity naming rules, custom tokens, and examples." },
3960
+ { usage: 'coop naming preview "Natural-language COOP command recommender" --entity task', purpose: "Preview the generated ID before creating an item." },
3961
+ { usage: "coop naming set task <TYPE>-<TITLE8>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand; `TITLE##` supports arbitrary 1-2 digit caps." },
3962
+ { usage: "coop naming reset task", purpose: "Reset one entity's naming template back to the default." },
3963
+ { usage: "coop naming token create proj", purpose: "Create a custom naming token." },
3964
+ { usage: "coop naming token remove proj", purpose: "Delete a custom naming token." },
3965
+ { usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." },
3966
+ { usage: "coop naming token value remove proj UX", purpose: "Remove an allowed value from a naming token." }
3476
3967
  ]
3477
3968
  },
3478
3969
  {
@@ -3483,8 +3974,10 @@ var catalog = {
3483
3974
  { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
3484
3975
  { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
3485
3976
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3977
+ { usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
3978
+ { usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
3486
3979
  { usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
3487
- { usage: "coop create track --id mvp --name MVP", purpose: "Create a named track before tasks refer to it." },
3980
+ { usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
3488
3981
  {
3489
3982
  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',
3490
3983
  purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
@@ -3545,6 +4038,7 @@ var catalog = {
3545
4038
  { usage: "coop update PM-101 --priority p1 --add-fix-version v2", purpose: "Update task metadata without editing `.coop` files directly." },
3546
4039
  { usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
3547
4040
  { usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
4041
+ { usage: "coop alias remove PM-101 PAY.UPI", purpose: "Remove an alias from a task or idea." },
3548
4042
  { usage: "coop plan delivery MVP", purpose: "Run delivery feasibility analysis." },
3549
4043
  { usage: "coop plan delivery MVP --monte-carlo --iterations 5000", purpose: "Run probabilistic delivery forecasting." },
3550
4044
  { usage: "coop view velocity", purpose: "Show historical throughput." },
@@ -3628,9 +4122,12 @@ function renderTopicPayload(topic) {
3628
4122
  return {
3629
4123
  topic,
3630
4124
  naming_guidance: [
3631
- "Use `coop naming` to inspect the current naming template and token behavior.",
3632
- 'Use `coop naming preview "<title>"` before creating a new idea, task, or delivery if predictable IDs matter.',
3633
- "Use `coop config id.naming ...` to override the default semantic naming template."
4125
+ "Use `coop naming` to inspect per-entity templates, custom tokens, and examples.",
4126
+ 'Use `coop naming preview "<title>" --entity <entity>` before creating a new item if predictable IDs matter.',
4127
+ "Use `coop naming set <entity> <template>` to update one entity's naming rule.",
4128
+ "Built-in title tokens support `TITLE##` with a 1-2 digit cap, such as `TITLE18`, `TITLE12`, `TITLE8`, `TITLE08`, `TITLE4`, or `TITLE02`.",
4129
+ "Use `coop naming reset <entity>` to revert one entity back to the default naming template.",
4130
+ "Use `coop naming token create <token>`, `coop naming token remove <token>`, and `coop naming token value add|remove <token> <value>` before passing custom token flags like `--proj UX`."
3634
4131
  ]
3635
4132
  };
3636
4133
  }
@@ -4358,7 +4855,13 @@ id_prefixes:
4358
4855
  run: "RUN"
4359
4856
 
4360
4857
  id:
4361
- naming: ${JSON.stringify(namingTemplate)}
4858
+ naming:
4859
+ task: ${JSON.stringify(namingTemplate)}
4860
+ idea: ${JSON.stringify(namingTemplate)}
4861
+ track: "<NAME_SLUG>"
4862
+ delivery: "<NAME_SLUG>"
4863
+ run: "<TYPE>-<YYMMDD>-<RAND>"
4864
+ tokens: {}
4362
4865
  seq_padding: 0
4363
4866
 
4364
4867
  defaults:
@@ -4720,15 +5223,14 @@ import path15 from "path";
4720
5223
  import {
4721
5224
  effective_priority as effective_priority3,
4722
5225
  load_graph as load_graph6,
4723
- parseDeliveryFile as parseDeliveryFile2,
4724
5226
  parseIdeaFile as parseIdeaFile4,
4725
5227
  parseTaskFile as parseTaskFile10,
4726
- parseYamlFile as parseYamlFile3,
4727
5228
  schedule_next as schedule_next3
4728
5229
  } from "@kitsy/coop-core";
4729
5230
  import chalk2 from "chalk";
4730
5231
  var TASK_COLUMN_WIDTHS = {
4731
5232
  id: 24,
5233
+ short: 12,
4732
5234
  title: 30,
4733
5235
  status: 12,
4734
5236
  priority: 4,
@@ -4740,6 +5242,7 @@ var TASK_COLUMN_WIDTHS = {
4740
5242
  };
4741
5243
  var IDEA_COLUMN_WIDTHS = {
4742
5244
  id: 24,
5245
+ short: 12,
4743
5246
  title: 30,
4744
5247
  status: 12,
4745
5248
  file: 30
@@ -4786,36 +5289,36 @@ function parseColumns(input2) {
4786
5289
  }
4787
5290
  function normalizeTaskColumns(value, ready) {
4788
5291
  if (!value?.trim()) {
4789
- return ready ? ["id", "title", "priority", "status", "assignee", "score"] : ["id", "title", "priority", "status", "assignee"];
5292
+ return ready ? ["id", "title", "priority", "status", "assignee", "track", "delivery", "score"] : ["id", "title", "priority", "status", "assignee", "track", "delivery"];
4790
5293
  }
4791
5294
  const raw = parseColumns(value);
4792
5295
  if (raw.length === 1 && raw[0] === "all") {
4793
- return ["id", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
5296
+ return ["id", "short", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
4794
5297
  }
4795
5298
  const normalized = raw.map((column) => {
4796
5299
  if (column === "p") return "priority";
4797
5300
  return column;
4798
5301
  });
4799
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
5302
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
4800
5303
  for (const column of normalized) {
4801
5304
  if (!valid.has(column)) {
4802
- throw new Error(`Invalid task column '${column}'. Expected id|title|status|priority|assignee|track|delivery|score|file|all.`);
5305
+ throw new Error(`Invalid task column '${column}'. Expected id|short|title|status|priority|assignee|track|delivery|score|file|all.`);
4803
5306
  }
4804
5307
  }
4805
5308
  return normalized;
4806
5309
  }
4807
5310
  function normalizeIdeaColumns(value) {
4808
5311
  if (!value?.trim()) {
4809
- return ["id", "title", "status"];
5312
+ return ["id", "short", "title", "status"];
4810
5313
  }
4811
5314
  const raw = parseColumns(value);
4812
5315
  if (raw.length === 1 && raw[0] === "all") {
4813
- return ["id", "title", "status", "file"];
5316
+ return ["id", "short", "title", "status", "file"];
4814
5317
  }
4815
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "file"]);
5318
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "file"]);
4816
5319
  for (const column of raw) {
4817
5320
  if (!valid.has(column)) {
4818
- throw new Error(`Invalid idea column '${column}'. Expected id|title|status|file|all.`);
5321
+ throw new Error(`Invalid idea column '${column}'. Expected id|short|title|status|file|all.`);
4819
5322
  }
4820
5323
  }
4821
5324
  return raw;
@@ -4952,6 +5455,8 @@ function sortTaskRows(rows, sortMode, readyOrder, track) {
4952
5455
  }
4953
5456
  function taskColumnHeader(column) {
4954
5457
  switch (column) {
5458
+ case "short":
5459
+ return "Short";
4955
5460
  case "priority":
4956
5461
  return "P";
4957
5462
  case "assignee":
@@ -4975,6 +5480,8 @@ function taskColumnHeader(column) {
4975
5480
  }
4976
5481
  function ideaColumnHeader(column) {
4977
5482
  switch (column) {
5483
+ case "short":
5484
+ return "Short";
4978
5485
  case "file":
4979
5486
  return "File";
4980
5487
  case "status":
@@ -5051,6 +5558,7 @@ function listTasks(options) {
5051
5558
  }).map(({ task, filePath }) => ({
5052
5559
  task,
5053
5560
  id: task.id,
5561
+ shortId: task.short_id ?? "-",
5054
5562
  title: task.title,
5055
5563
  status: task.status,
5056
5564
  priority: taskEffectivePriority(task, resolvedTrack.value),
@@ -5071,10 +5579,12 @@ function listTasks(options) {
5071
5579
  (entry) => columns.map((column) => {
5072
5580
  const rawValue = (() => {
5073
5581
  switch (column) {
5582
+ case "short":
5583
+ return entry.shortId;
5074
5584
  case "title":
5075
5585
  return entry.title;
5076
5586
  case "status":
5077
- return statusColor(entry.status);
5587
+ return entry.status;
5078
5588
  case "priority":
5079
5589
  return entry.priority;
5080
5590
  case "assignee":
@@ -5092,7 +5602,8 @@ function listTasks(options) {
5092
5602
  return entry.id;
5093
5603
  }
5094
5604
  })();
5095
- return column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
5605
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
5606
+ return column === "status" ? statusColor(truncated) : truncated;
5096
5607
  })
5097
5608
  )
5098
5609
  )
@@ -5110,6 +5621,7 @@ function listIdeas(options) {
5110
5621
  return true;
5111
5622
  }).map(({ idea, filePath }) => ({
5112
5623
  id: idea.id,
5624
+ shortId: idea.short_id ?? "-",
5113
5625
  title: idea.title,
5114
5626
  status: idea.status,
5115
5627
  priority: "-",
@@ -5142,8 +5654,9 @@ function listIdeas(options) {
5142
5654
  columns.map(ideaColumnHeader),
5143
5655
  sorted.map(
5144
5656
  (entry) => columns.map((column) => {
5145
- const rawValue = column === "title" ? entry.title : column === "status" ? statusColor(entry.status) : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
5146
- return column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
5657
+ const rawValue = column === "short" ? entry.shortId : column === "title" ? entry.title : column === "status" ? entry.status : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
5658
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
5659
+ return column === "status" ? statusColor(truncated) : truncated;
5147
5660
  })
5148
5661
  )
5149
5662
  )
@@ -5153,10 +5666,11 @@ Total ideas: ${sorted.length}`);
5153
5666
  }
5154
5667
  function listDeliveries() {
5155
5668
  const root = resolveRepoRoot();
5156
- const rows = listDeliveryFiles(root).map((filePath) => ({ delivery: parseDeliveryFile2(filePath).delivery, filePath })).map(({ delivery, filePath }) => [
5669
+ const rows = loadDeliveryEntries(root).map(({ delivery, filePath }) => [
5157
5670
  truncateCell(delivery.id, 24),
5671
+ truncateCell(delivery.short_id ?? "-", 12),
5158
5672
  truncateCell(delivery.name, 30),
5159
- truncateCell(statusColor(delivery.status), 12),
5673
+ statusColor(truncateCell(delivery.status, 12)),
5160
5674
  truncateCell(delivery.target_date ?? "-", 16),
5161
5675
  truncateMiddleCell(path15.relative(root, filePath), 30)
5162
5676
  ]);
@@ -5164,12 +5678,13 @@ function listDeliveries() {
5164
5678
  console.log("No deliveries found.");
5165
5679
  return;
5166
5680
  }
5167
- console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
5681
+ console.log(formatTable(["ID", "Short", "Name", "Status", "Target", "File"], rows));
5168
5682
  }
5169
5683
  function listTracks() {
5170
5684
  const root = resolveRepoRoot();
5171
- const rows = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile3(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id)).map(({ track, filePath }) => [
5685
+ const rows = loadTrackEntries(root).map(({ track, filePath }) => [
5172
5686
  truncateCell(track.id, 24),
5687
+ truncateCell(track.short_id ?? "-", 12),
5173
5688
  truncateCell(track.name, 30),
5174
5689
  truncateCell((track.capacity_profiles ?? []).join(", ") || "-", 24),
5175
5690
  truncateCell(String(track.constraints?.max_concurrent_tasks ?? "-"), 8),
@@ -5179,14 +5694,14 @@ function listTracks() {
5179
5694
  console.log("No tracks found.");
5180
5695
  return;
5181
5696
  }
5182
- console.log(formatTable(["ID", "Name", "Profiles", "Max WIP", "File"], rows));
5697
+ console.log(formatTable(["ID", "Short", "Name", "Profiles", "Max WIP", "File"], rows));
5183
5698
  }
5184
5699
  function registerListCommand(program) {
5185
5700
  const list = program.command("list").description("List COOP entities");
5186
- list.command("tasks").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
5701
+ 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) => {
5187
5702
  listTasks(options);
5188
5703
  });
5189
- 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) => {
5704
+ 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) => {
5190
5705
  listIdeas(options);
5191
5706
  });
5192
5707
  list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
@@ -5202,18 +5717,21 @@ function registerListCommand(program) {
5202
5717
 
5203
5718
  // src/utils/logger.ts
5204
5719
  import fs11 from "fs";
5720
+ import os2 from "os";
5205
5721
  import path16 from "path";
5206
5722
  function resolveWorkspaceRoot(start = process.cwd()) {
5207
5723
  let current = path16.resolve(start);
5724
+ const configuredCoopHome = path16.resolve(resolveCoopHome());
5725
+ const defaultCoopHome = path16.resolve(path16.join(os2.homedir(), ".coop"));
5208
5726
  while (true) {
5209
5727
  const gitDir = path16.join(current, ".git");
5210
5728
  const coopDir2 = coopWorkspaceDir(current);
5211
5729
  const workspaceConfig = path16.join(coopDir2, "config.yml");
5212
- const projectsDir = path16.join(coopDir2, "projects");
5213
5730
  if (fs11.existsSync(gitDir)) {
5214
5731
  return current;
5215
5732
  }
5216
- if (fs11.existsSync(workspaceConfig) || fs11.existsSync(projectsDir)) {
5733
+ const resolvedCoopDir = path16.resolve(coopDir2);
5734
+ if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
5217
5735
  return current;
5218
5736
  }
5219
5737
  const parent = path16.dirname(current);
@@ -5340,7 +5858,7 @@ import fs12 from "fs";
5340
5858
  import path17 from "path";
5341
5859
  import { createInterface } from "readline/promises";
5342
5860
  import { stdin as input, stdout as output } from "process";
5343
- import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile4, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
5861
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile5 } from "@kitsy/coop-core";
5344
5862
  var COOP_IGNORE_TEMPLATE2 = `.index/
5345
5863
  logs/
5346
5864
  tmp/
@@ -5507,7 +6025,7 @@ async function migrateWorkspaceLayout(root, options) {
5507
6025
  }
5508
6026
  const movedConfigPath = path17.join(projectRoot, "config.yml");
5509
6027
  if (fs12.existsSync(movedConfigPath)) {
5510
- const movedConfig = parseYamlFile4(movedConfigPath);
6028
+ const movedConfig = parseYamlFile5(movedConfigPath);
5511
6029
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
5512
6030
  nextProject.name = identity.projectName;
5513
6031
  nextProject.id = projectId;
@@ -5515,7 +6033,7 @@ async function migrateWorkspaceLayout(root, options) {
5515
6033
  const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
5516
6034
  nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
5517
6035
  nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
5518
- writeYamlFile4(movedConfigPath, {
6036
+ writeYamlFile5(movedConfigPath, {
5519
6037
  ...movedConfig,
5520
6038
  project: nextProject,
5521
6039
  hooks: nextHooks
@@ -5569,25 +6087,54 @@ function registerMigrateCommand(program) {
5569
6087
  }
5570
6088
 
5571
6089
  // src/commands/naming.ts
6090
+ var ENTITY_NAMES = ["task", "idea", "track", "delivery", "run"];
6091
+ function assertNamingEntity(value) {
6092
+ const entity = value?.trim().toLowerCase() || "task";
6093
+ if (!ENTITY_NAMES.includes(entity)) {
6094
+ throw new Error(`Invalid entity '${value}'. Expected ${ENTITY_NAMES.join("|")}.`);
6095
+ }
6096
+ return entity;
6097
+ }
6098
+ function readConfigRecord(root) {
6099
+ return readCoopConfig(root).raw;
6100
+ }
6101
+ function writeNamingConfig(root, update) {
6102
+ writeCoopConfig(root, update(readConfigRecord(root)));
6103
+ }
5572
6104
  function printNamingOverview() {
5573
6105
  const root = resolveRepoRoot();
5574
6106
  const config = readCoopConfig(root);
6107
+ const templates = namingTemplatesForRoot(root);
6108
+ const tokens = namingTokensForRoot(root);
5575
6109
  const sampleTitle = "Natural-language COOP command recommender";
5576
6110
  const sampleTaskTitle = "Implement billing payment contract review";
5577
6111
  console.log("COOP Naming");
5578
- console.log(`Current template: ${config.idNamingTemplate}`);
5579
- console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
5580
- console.log("Tokens:");
6112
+ console.log(`Legacy task template: ${config.idNamingTemplate}`);
6113
+ console.log(`Default task template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
6114
+ console.log("Per-entity templates:");
6115
+ for (const entity of ENTITY_NAMES) {
6116
+ console.log(`- ${entity}: ${templates[entity]}`);
6117
+ }
6118
+ console.log("Built-in tokens:");
5581
6119
  console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
5582
6120
  console.log(" <TITLE> semantic title token (defaults to TITLE16)");
5583
- console.log(" <TITLE16> semantic title token capped to 16 chars");
5584
- console.log(" <TITLE24> semantic title token capped to 24 chars");
6121
+ console.log(" <TITLE##> semantic title token capped to the numeric suffix, e.g. TITLE18, TITLE8, or TITLE08");
5585
6122
  console.log(" <TRACK> task track");
6123
+ console.log(" <NAME> entity name/title");
6124
+ console.log(" <NAME_SLUG> lower-case slug of the entity name");
5586
6125
  console.log(" <SEQ> sequential number within the rendered pattern");
5587
6126
  console.log(" <USER> actor/user namespace");
5588
6127
  console.log(" <YYMMDD> short date token");
5589
6128
  console.log(" <RAND> random uniqueness token");
5590
6129
  console.log(" <PREFIX> entity prefix override");
6130
+ console.log("Custom tokens:");
6131
+ if (Object.keys(tokens).length === 0) {
6132
+ console.log("- none");
6133
+ } else {
6134
+ for (const [token, definition] of Object.entries(tokens)) {
6135
+ console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6136
+ }
6137
+ }
5591
6138
  console.log("Examples:");
5592
6139
  const tokenExamples = namingTokenExamples(sampleTitle);
5593
6140
  for (const [token, value] of Object.entries(tokenExamples)) {
@@ -5597,7 +6144,7 @@ function printNamingOverview() {
5597
6144
  entityType: "idea",
5598
6145
  title: sampleTitle
5599
6146
  }, root)}`);
5600
- console.log(`Preview (${config.idNamingTemplate}): ${previewNamingTemplate(config.idNamingTemplate, {
6147
+ console.log(`Preview (${templates.task}): ${previewNamingTemplate(templates.task, {
5601
6148
  entityType: "task",
5602
6149
  title: sampleTaskTitle,
5603
6150
  track: "mvp",
@@ -5606,35 +6153,204 @@ function printNamingOverview() {
5606
6153
  }, root)}`);
5607
6154
  console.log("Try:");
5608
6155
  console.log(` coop naming preview "${sampleTitle}"`);
5609
- console.log(' coop config id.naming "<TYPE>-<TITLE24>"');
5610
- console.log(' coop config id.naming "<TRACK>-<SEQ>"');
6156
+ console.log(" coop naming set task <TYPE>-<TITLE8>-<SEQ>");
6157
+ console.log(" coop naming token create proj");
6158
+ console.log(" coop naming token value add proj UX");
6159
+ }
6160
+ function setEntityNamingTemplate(root, entity, template) {
6161
+ const nextTemplate = template.trim();
6162
+ if (!nextTemplate) {
6163
+ throw new Error("Naming template must be non-empty.");
6164
+ }
6165
+ writeNamingConfig(root, (config) => {
6166
+ const next = { ...config };
6167
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6168
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
6169
+ namingRaw[entity] = nextTemplate;
6170
+ idRaw.naming = namingRaw;
6171
+ next.id = idRaw;
6172
+ return next;
6173
+ });
6174
+ }
6175
+ function resetEntityNamingTemplate(root, entity) {
6176
+ writeNamingConfig(root, (config) => {
6177
+ const next = { ...config };
6178
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6179
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
6180
+ namingRaw[entity] = DEFAULT_NAMING_TEMPLATES[entity];
6181
+ idRaw.naming = namingRaw;
6182
+ next.id = idRaw;
6183
+ return next;
6184
+ });
6185
+ }
6186
+ function ensureTokenRecord(config) {
6187
+ const next = { ...config };
6188
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6189
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? { ...idRaw.tokens } : {};
6190
+ idRaw.tokens = tokensRaw;
6191
+ next.id = idRaw;
6192
+ return { next, tokens: tokensRaw };
6193
+ }
6194
+ function listTokens() {
6195
+ const root = resolveRepoRoot();
6196
+ const tokens = namingTokensForRoot(root);
6197
+ if (Object.keys(tokens).length === 0) {
6198
+ console.log("No naming tokens defined.");
6199
+ return;
6200
+ }
6201
+ for (const [token, definition] of Object.entries(tokens)) {
6202
+ console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6203
+ }
5611
6204
  }
5612
6205
  function registerNamingCommand(program) {
5613
- const naming = program.command("naming").description("Explain COOP ID naming templates and preview examples");
6206
+ const naming = program.command("naming").description("Manage COOP naming templates, tokens, and previews");
5614
6207
  naming.action(() => {
5615
6208
  printNamingOverview();
5616
6209
  });
5617
- 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(
6210
+ 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(
5618
6211
  (title, options) => {
5619
6212
  const root = resolveRepoRoot();
5620
- const config = readCoopConfig(root);
5621
- const entity = options.entity?.trim().toLowerCase() || "task";
5622
- const template = options.template?.trim() || config.idNamingTemplate;
6213
+ const entity = assertNamingEntity(options.entity);
6214
+ const templates = namingTemplatesForRoot(root);
6215
+ const dynamicFields = extractDynamicTokenFlags(
6216
+ ["naming", "preview"],
6217
+ ["template", "entity", "track", "status", "task-type"]
6218
+ );
6219
+ const tokens = namingTokensForRoot(root);
6220
+ for (const key of Object.keys(dynamicFields)) {
6221
+ if (!tokens[key]) {
6222
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
6223
+ }
6224
+ }
5623
6225
  console.log(
5624
6226
  previewNamingTemplate(
5625
- template,
6227
+ options.template?.trim() || templates[entity],
5626
6228
  {
5627
6229
  entityType: entity,
5628
6230
  title,
6231
+ name: title,
5629
6232
  track: options.track,
5630
6233
  status: options.status,
5631
- taskType: options.taskType
6234
+ taskType: options.taskType,
6235
+ fields: dynamicFields
5632
6236
  },
5633
6237
  root
5634
6238
  )
5635
6239
  );
5636
6240
  }
5637
6241
  );
6242
+ 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) => {
6243
+ const root = resolveRepoRoot();
6244
+ setEntityNamingTemplate(root, assertNamingEntity(entity), template);
6245
+ console.log(`${assertNamingEntity(entity)}=${namingTemplatesForRoot(root)[assertNamingEntity(entity)]}`);
6246
+ });
6247
+ naming.command("reset").description("Reset the naming template for one entity type to the default").argument("<entity>", "Entity: task|idea|track|delivery|run").action((entity) => {
6248
+ const root = resolveRepoRoot();
6249
+ const resolved = assertNamingEntity(entity);
6250
+ resetEntityNamingTemplate(root, resolved);
6251
+ console.log(`${resolved}=${namingTemplatesForRoot(root)[resolved]}`);
6252
+ });
6253
+ const token = naming.command("token").description("Manage custom naming tokens");
6254
+ token.command("list").description("List naming tokens").action(() => {
6255
+ listTokens();
6256
+ });
6257
+ token.command("create").description("Create a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6258
+ const root = resolveRepoRoot();
6259
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6260
+ writeNamingConfig(root, (config) => {
6261
+ const { next, tokens } = ensureTokenRecord(config);
6262
+ if (tokens[normalizedToken]) {
6263
+ throw new Error(`Naming token '${normalizedToken}' already exists.`);
6264
+ }
6265
+ tokens[normalizedToken] = { values: [] };
6266
+ return next;
6267
+ });
6268
+ console.log(`Created naming token: ${normalizedToken}`);
6269
+ });
6270
+ token.command("rename").description("Rename a custom naming token").argument("<token>", "Existing token name").argument("<next>", "New token name").action((tokenName, nextToken) => {
6271
+ const root = resolveRepoRoot();
6272
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6273
+ const normalizedNext = normalizeNamingTokenName(nextToken);
6274
+ const tokens = namingTokensForRoot(root);
6275
+ if (!tokens[normalizedToken]) {
6276
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6277
+ }
6278
+ if (tokens[normalizedNext]) {
6279
+ throw new Error(`Naming token '${normalizedNext}' already exists.`);
6280
+ }
6281
+ writeNamingConfig(root, (config) => {
6282
+ const { next, tokens: tokensRaw } = ensureTokenRecord(config);
6283
+ tokensRaw[normalizedNext] = tokensRaw[normalizedToken];
6284
+ delete tokensRaw[normalizedToken];
6285
+ return next;
6286
+ });
6287
+ console.log(`Renamed naming token: ${normalizedToken} -> ${normalizedNext}`);
6288
+ });
6289
+ token.command("delete").alias("remove").alias("rm").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6290
+ const root = resolveRepoRoot();
6291
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6292
+ writeNamingConfig(root, (config) => {
6293
+ const { next, tokens } = ensureTokenRecord(config);
6294
+ delete tokens[normalizedToken];
6295
+ return next;
6296
+ });
6297
+ console.log(`Deleted naming token: ${normalizedToken}`);
6298
+ });
6299
+ const tokenValue = token.command("value").description("Manage allowed values for a naming token");
6300
+ tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
6301
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6302
+ const tokens = namingTokensForRoot(resolveRepoRoot());
6303
+ const values = tokens[normalizedToken]?.values;
6304
+ if (!values) {
6305
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6306
+ }
6307
+ if (values.length === 0) {
6308
+ console.log("(no values)");
6309
+ return;
6310
+ }
6311
+ for (const value of values) {
6312
+ console.log(value);
6313
+ }
6314
+ });
6315
+ tokenValue.command("add").description("Add an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6316
+ const root = resolveRepoRoot();
6317
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6318
+ const normalizedValue = normalizeNamingTokenValue(value);
6319
+ writeNamingConfig(root, (config) => {
6320
+ const { next, tokens } = ensureTokenRecord(config);
6321
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6322
+ if (!tokenRecord) {
6323
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6324
+ }
6325
+ const values = Array.isArray(tokenRecord.values) ? Array.from(
6326
+ new Set(
6327
+ tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
6328
+ )
6329
+ ) : [];
6330
+ values.push(normalizedValue);
6331
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6332
+ tokens[normalizedToken] = tokenRecord;
6333
+ return next;
6334
+ });
6335
+ console.log(`Added naming token value: ${normalizedToken}=${normalizedValue}`);
6336
+ });
6337
+ tokenValue.command("remove").description("Remove an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6338
+ const root = resolveRepoRoot();
6339
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6340
+ const normalizedValue = normalizeNamingTokenValue(value);
6341
+ writeNamingConfig(root, (config) => {
6342
+ const { next, tokens } = ensureTokenRecord(config);
6343
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6344
+ if (!tokenRecord) {
6345
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6346
+ }
6347
+ 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) : [];
6348
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6349
+ tokens[normalizedToken] = tokenRecord;
6350
+ return next;
6351
+ });
6352
+ console.log(`Removed naming token value: ${normalizedToken}=${normalizedValue}`);
6353
+ });
5638
6354
  }
5639
6355
 
5640
6356
  // src/commands/plan.ts
@@ -5955,7 +6671,13 @@ id_prefixes:
5955
6671
  run: "RUN"
5956
6672
 
5957
6673
  id:
5958
- naming: ${JSON.stringify(namingTemplate)}
6674
+ naming:
6675
+ task: ${JSON.stringify(namingTemplate)}
6676
+ idea: ${JSON.stringify(namingTemplate)}
6677
+ track: "<NAME_SLUG>"
6678
+ delivery: "<NAME_SLUG>"
6679
+ run: "<TYPE>-<YYMMDD>-<RAND>"
6680
+ tokens: {}
5959
6681
  seq_padding: 0
5960
6682
 
5961
6683
  defaults:
@@ -6375,7 +7097,7 @@ function registerRunCommand(program) {
6375
7097
  }
6376
7098
 
6377
7099
  // src/commands/search.ts
6378
- import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile3, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
7100
+ import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile4, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
6379
7101
  function haystackForTask(task) {
6380
7102
  return [
6381
7103
  task.id,
@@ -6454,7 +7176,7 @@ ${parsed.body}`, query)) continue;
6454
7176
  }
6455
7177
  if (options.kind === "all" || options.kind === "delivery") {
6456
7178
  for (const filePath of listDeliveryFiles(root)) {
6457
- const parsed = parseDeliveryFile3(filePath);
7179
+ const parsed = parseDeliveryFile4(filePath);
6458
7180
  if (options.status && parsed.delivery.status !== options.status) continue;
6459
7181
  if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
6460
7182
  rows.push({
@@ -6798,6 +7520,7 @@ function showTask(taskId, options = {}) {
6798
7520
  if (options.compact) {
6799
7521
  const compactLines = [
6800
7522
  `Task: ${task.id}`,
7523
+ `Short ID: ${task.short_id ?? "-"}`,
6801
7524
  `Title: ${task.title}`,
6802
7525
  `Status: ${task.status}`,
6803
7526
  `Priority: ${task.priority ?? "-"}`,
@@ -6815,6 +7538,7 @@ function showTask(taskId, options = {}) {
6815
7538
  }
6816
7539
  const lines = [
6817
7540
  `Task: ${task.id}`,
7541
+ `Short ID: ${task.short_id ?? "-"}`,
6818
7542
  `Title: ${task.title}`,
6819
7543
  `Status: ${task.status}`,
6820
7544
  `Type: ${task.type}`,
@@ -6888,6 +7612,7 @@ function showIdea(ideaId, options = {}) {
6888
7612
  console.log(
6889
7613
  [
6890
7614
  `Idea: ${idea.id}`,
7615
+ `Short ID: ${idea.short_id ?? "-"}`,
6891
7616
  `Title: ${idea.title}`,
6892
7617
  `Status: ${idea.status}`,
6893
7618
  `Tags: ${stringify(idea.tags)}`,
@@ -6899,6 +7624,7 @@ function showIdea(ideaId, options = {}) {
6899
7624
  }
6900
7625
  const lines = [
6901
7626
  `Idea: ${idea.id}`,
7627
+ `Short ID: ${idea.short_id ?? "-"}`,
6902
7628
  `Title: ${idea.title}`,
6903
7629
  `Status: ${idea.status}`,
6904
7630
  `Author: ${idea.author}`,
@@ -6921,6 +7647,7 @@ function showDelivery(ref, options = {}) {
6921
7647
  console.log(
6922
7648
  [
6923
7649
  `Delivery: ${delivery.id}`,
7650
+ `Short ID: ${delivery.short_id ?? "-"}`,
6924
7651
  `Name: ${delivery.name}`,
6925
7652
  `Status: ${delivery.status}`,
6926
7653
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6932,6 +7659,7 @@ function showDelivery(ref, options = {}) {
6932
7659
  }
6933
7660
  const lines = [
6934
7661
  `Delivery: ${delivery.id}`,
7662
+ `Short ID: ${delivery.short_id ?? "-"}`,
6935
7663
  `Name: ${delivery.name}`,
6936
7664
  `Status: ${delivery.status}`,
6937
7665
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6947,6 +7675,28 @@ function showDelivery(ref, options = {}) {
6947
7675
  ];
6948
7676
  console.log(lines.join("\n"));
6949
7677
  }
7678
+ function showTrack(ref, options = {}) {
7679
+ const root = resolveRepoRoot();
7680
+ const resolvedId = resolveExistingTrackId(root, ref);
7681
+ if (!resolvedId) {
7682
+ throw new Error(`Track '${ref}' not found.`);
7683
+ }
7684
+ const entry = loadTrackEntries(root).find((candidate) => candidate.track.id === resolvedId);
7685
+ if (!entry) {
7686
+ throw new Error(`Track '${ref}' not found.`);
7687
+ }
7688
+ const track = entry.track;
7689
+ const base = [
7690
+ `Track: ${track.id}`,
7691
+ `Short ID: ${track.short_id ?? "-"}`,
7692
+ `Name: ${track.name}`,
7693
+ `Profiles: ${(track.capacity_profiles ?? []).join(", ") || "-"}`,
7694
+ `Max WIP: ${track.constraints?.max_concurrent_tasks ?? "-"}`,
7695
+ `Allowed Types: ${(track.constraints?.allowed_types ?? []).join(", ") || "-"}`,
7696
+ `File: ${path22.relative(root, entry.filePath)}`
7697
+ ];
7698
+ console.log((options.compact ? base.filter((line) => !line.startsWith("Allowed Types")) : base).join("\n"));
7699
+ }
6950
7700
  function showByReference(ref, options = {}) {
6951
7701
  const root = resolveRepoRoot();
6952
7702
  try {
@@ -6958,7 +7708,11 @@ function showByReference(ref, options = {}) {
6958
7708
  showIdea(ref, options);
6959
7709
  return;
6960
7710
  } catch {
6961
- showDelivery(ref, options);
7711
+ try {
7712
+ showDelivery(ref, options);
7713
+ } catch {
7714
+ showTrack(ref, options);
7715
+ }
6962
7716
  }
6963
7717
  }
6964
7718
  function registerShowCommand(program) {
@@ -6974,6 +7728,9 @@ function registerShowCommand(program) {
6974
7728
  show.command("idea").description("Show idea details").argument("<id>", "Idea ID").option("--compact", "Show a smaller summary view").action((id, options) => {
6975
7729
  showIdea(id, options);
6976
7730
  });
7731
+ 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) => {
7732
+ showTrack(id, options);
7733
+ });
6977
7734
  show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").option("--compact", "Show a smaller summary view").action((id, options) => {
6978
7735
  showDelivery(id, options);
6979
7736
  });
@@ -7556,6 +8313,10 @@ function registerUseCommand(program) {
7556
8313
  const root = resolveRepoRoot();
7557
8314
  printContext(clearWorkingContext(root, resolveCoopHome(), scope));
7558
8315
  });
8316
+ use.command("reset").description("Clear all working-context values").action(() => {
8317
+ const root = resolveRepoRoot();
8318
+ printContext(clearWorkingContext(root, resolveCoopHome(), "all"));
8319
+ });
7559
8320
  }
7560
8321
 
7561
8322
  // src/commands/view.ts
@@ -7896,7 +8657,7 @@ function registerWebhookCommand(program) {
7896
8657
 
7897
8658
  // src/merge-driver/merge-driver.ts
7898
8659
  import fs21 from "fs";
7899
- import os2 from "os";
8660
+ import os3 from "os";
7900
8661
  import path25 from "path";
7901
8662
  import { spawnSync as spawnSync5 } from "child_process";
7902
8663
  import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
@@ -7987,7 +8748,7 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
7987
8748
  const ours = parseTaskDocument(oursRaw, oursPath);
7988
8749
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
7989
8750
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
7990
- const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
8751
+ const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
7991
8752
  try {
7992
8753
  const ancestorBody = path25.join(tempDir, "ancestor.md");
7993
8754
  const oursBody = path25.join(tempDir, "ours.md");
@@ -8033,14 +8794,18 @@ function renderBasicHelp() {
8033
8794
  "",
8034
8795
  "Day-to-day commands:",
8035
8796
  "- `coop current`: show working context, active work, and the next ready task",
8036
- "- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
8797
+ "- `coop use track <id>` / `coop use delivery <id>` / `coop use reset`: manage working scope defaults",
8037
8798
  "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
8799
+ "- `coop naming`: inspect per-entity ID rules and naming tokens",
8800
+ '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item; templates support `TITLE##` like `TITLE18`, `TITLE8`, or `TITLE08`',
8801
+ "- `coop naming reset task`: reset one entity's naming template to the default",
8038
8802
  "- `coop next task` or `coop pick task`: choose work from COOP",
8039
8803
  "- `coop show <id>`: inspect a task, idea, or delivery",
8040
8804
  "- `coop list tasks --track <id>`: browse scoped work",
8041
8805
  "- `coop update <id> --track <id> --delivery <id>`: update task metadata",
8042
8806
  '- `coop comment <id> --message "..."`: append a task comment',
8043
8807
  "- `coop log-time <id> --hours 2 --kind worked`: append time spent",
8808
+ "- `coop alias remove <id> <alias>`: remove a shorthand alias from a task or idea",
8044
8809
  "- `coop review task <id>` / `coop complete task <id>`: move work through lifecycle",
8045
8810
  "- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`: hand off COOP context to an agent",
8046
8811
  "",
@@ -8082,6 +8847,7 @@ Common day-to-day commands:
8082
8847
  coop current
8083
8848
  coop next task
8084
8849
  coop show <id>
8850
+ coop naming
8085
8851
  `);
8086
8852
  registerInitCommand(program);
8087
8853
  registerCreateCommand(program);