@kitsy/coop 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +828 -127
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -32,7 +32,31 @@ var SEQ_MARKER = "COOPSEQTOKEN";
32
32
  var DEFAULT_ID_NAMING_TEMPLATE = "<TYPE>-<TITLE16>-<SEQ>";
33
33
  var DEFAULT_ARTIFACTS_DIR = "docs";
34
34
  var DEFAULT_TITLE_TOKEN_LENGTH = 16;
35
+ var DEFAULT_NAMING_TEMPLATES = {
36
+ task: DEFAULT_ID_NAMING_TEMPLATE,
37
+ idea: DEFAULT_ID_NAMING_TEMPLATE,
38
+ track: "<NAME_SLUG>",
39
+ delivery: "<NAME_SLUG>",
40
+ run: "<TYPE>-<YYMMDD>-<RAND>"
41
+ };
35
42
  var SEMANTIC_WORD_MAX = 4;
43
+ var BUILT_IN_NAMING_TOKENS = /* @__PURE__ */ new Set([
44
+ "TYPE",
45
+ "ENTITY",
46
+ "TITLE",
47
+ "TITLE16",
48
+ "TITLE24",
49
+ "TRACK",
50
+ "STATUS",
51
+ "TASK_TYPE",
52
+ "PREFIX",
53
+ "USER",
54
+ "YYMMDD",
55
+ "RAND",
56
+ "SEQ",
57
+ "NAME",
58
+ "NAME_SLUG"
59
+ ]);
36
60
  var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
37
61
  "A",
38
62
  "AN",
@@ -60,8 +84,12 @@ function resolveCoopHome() {
60
84
  }
61
85
  function resolveRepoRoot(start = process.cwd()) {
62
86
  let current = path.resolve(start);
87
+ const configuredCoopHome = path.resolve(resolveCoopHome());
88
+ const defaultCoopHome = path.resolve(path.join(os.homedir(), ".coop"));
63
89
  while (true) {
64
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".coop"))) {
90
+ const coopDir2 = path.join(current, ".coop");
91
+ const isGlobalCoopHome = path.resolve(coopDir2) === configuredCoopHome || path.resolve(coopDir2) === defaultCoopHome;
92
+ if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(coopDir2) && !isGlobalCoopHome) {
65
93
  return current;
66
94
  }
67
95
  const parent = path.dirname(current);
@@ -126,6 +154,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
126
154
  taskPrefix: "PM",
127
155
  indexDataFormat: "yaml",
128
156
  idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
157
+ idNamingTemplates: { ...DEFAULT_NAMING_TEMPLATES },
158
+ idTokens: {},
129
159
  idSeqPadding: 0,
130
160
  artifactsDir: DEFAULT_ARTIFACTS_DIR,
131
161
  projectName: repoName || "COOP Workspace",
@@ -147,9 +177,10 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
147
177
  const indexRaw = typeof config.index === "object" && config.index !== null ? config.index : {};
148
178
  const indexDataRaw = indexRaw.data;
149
179
  const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
180
+ const idNamingTemplates = readNamingTemplates(config);
181
+ const idNamingTemplate = idNamingTemplates.task;
182
+ const idTokens = readNamingTokens(config);
150
183
  const idRaw = typeof config.id === "object" && config.id !== null ? config.id : {};
151
- const idNamingTemplateRaw = idRaw.naming;
152
- const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : DEFAULT_ID_NAMING_TEMPLATE;
153
184
  const idSeqPaddingRaw = idRaw.seq_padding;
154
185
  const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
155
186
  const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
@@ -159,6 +190,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
159
190
  taskPrefix,
160
191
  indexDataFormat,
161
192
  idNamingTemplate,
193
+ idNamingTemplates,
194
+ idTokens,
162
195
  idSeqPadding,
163
196
  artifactsDir,
164
197
  projectName: projectName || "COOP Workspace",
@@ -176,6 +209,59 @@ function sanitizeIdentityPart(value, fallback) {
176
209
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
177
210
  return normalized || fallback;
178
211
  }
212
+ function slugifyLower(value, fallback = "item") {
213
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
214
+ return normalized || fallback;
215
+ }
216
+ function normalizeNamingTokenName(value) {
217
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
218
+ if (!normalized) {
219
+ throw new Error("Naming token names must contain letters, numbers, or underscores.");
220
+ }
221
+ return normalized;
222
+ }
223
+ function normalizeNamingTokenValue(value) {
224
+ const normalized = value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
225
+ if (!normalized) {
226
+ throw new Error("Naming token values must contain letters or numbers.");
227
+ }
228
+ return normalized;
229
+ }
230
+ function readNamingTemplates(rawConfig) {
231
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
232
+ const namingRaw = idRaw.naming;
233
+ if (typeof namingRaw === "string" && namingRaw.trim().length > 0) {
234
+ return {
235
+ ...DEFAULT_NAMING_TEMPLATES,
236
+ task: namingRaw.trim(),
237
+ idea: namingRaw.trim()
238
+ };
239
+ }
240
+ const namingRecord = typeof namingRaw === "object" && namingRaw !== null ? namingRaw : {};
241
+ return {
242
+ task: typeof namingRecord.task === "string" && namingRecord.task.trim().length > 0 ? namingRecord.task.trim() : DEFAULT_NAMING_TEMPLATES.task,
243
+ idea: typeof namingRecord.idea === "string" && namingRecord.idea.trim().length > 0 ? namingRecord.idea.trim() : DEFAULT_NAMING_TEMPLATES.idea,
244
+ track: typeof namingRecord.track === "string" && namingRecord.track.trim().length > 0 ? namingRecord.track.trim() : DEFAULT_NAMING_TEMPLATES.track,
245
+ delivery: typeof namingRecord.delivery === "string" && namingRecord.delivery.trim().length > 0 ? namingRecord.delivery.trim() : DEFAULT_NAMING_TEMPLATES.delivery,
246
+ run: typeof namingRecord.run === "string" && namingRecord.run.trim().length > 0 ? namingRecord.run.trim() : DEFAULT_NAMING_TEMPLATES.run
247
+ };
248
+ }
249
+ function readNamingTokens(rawConfig) {
250
+ const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
251
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? idRaw.tokens : {};
252
+ const tokens = {};
253
+ for (const [rawName, rawValue] of Object.entries(tokensRaw)) {
254
+ const name = normalizeNamingTokenName(rawName);
255
+ const valuesRecord = typeof rawValue === "object" && rawValue !== null ? rawValue : {};
256
+ const values = Array.isArray(valuesRecord.values) ? Array.from(
257
+ new Set(
258
+ valuesRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
259
+ )
260
+ ).sort((a, b) => a.localeCompare(b)) : [];
261
+ tokens[name] = { values };
262
+ }
263
+ return tokens;
264
+ }
179
265
  function repoDisplayName(root) {
180
266
  const base = path.basename(path.resolve(root)).trim();
181
267
  return base || "COOP Workspace";
@@ -278,6 +364,19 @@ function sanitizeTemplateValue(input2, fallback = "X") {
278
364
  const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
279
365
  return normalized || fallback;
280
366
  }
367
+ function normalizeEntityIdValue(entityType, input2, fallback = "COOP-ID") {
368
+ if (entityType === "track" || entityType === "delivery") {
369
+ return slugifyLower(input2, slugifyLower(fallback, "item"));
370
+ }
371
+ return sanitizeTemplateValue(input2, fallback);
372
+ }
373
+ function extractTemplateTokens(template) {
374
+ return Array.from(
375
+ new Set(
376
+ Array.from(template.matchAll(/<([^>]+)>/g)).map((match) => match[1]?.trim().toUpperCase()).filter((token) => Boolean(token))
377
+ )
378
+ );
379
+ }
281
380
  function sanitizeSemanticWord(input2) {
282
381
  return input2.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
283
382
  }
@@ -332,8 +431,8 @@ function namingTokenExamples(title = "Natural-language COOP command recommender"
332
431
  };
333
432
  }
334
433
  function sequenceForPattern(existingIds, prefix, suffix) {
335
- const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
336
- const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
434
+ const escapedPrefix = prefix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
435
+ const escapedSuffix = suffix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
337
436
  const regex = new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`);
338
437
  let max = 0;
339
438
  for (const id of existingIds) {
@@ -359,6 +458,7 @@ function buildIdContext(root, config, context) {
359
458
  const track = context.track?.trim() || "";
360
459
  const status = context.status?.trim() || "";
361
460
  const title = context.title?.trim() || "";
461
+ const name = context.name?.trim() || context.title?.trim() || "";
362
462
  const prefix = context.prefix?.trim() || (context.entityType === "idea" ? config.ideaPrefix : config.taskPrefix);
363
463
  const actor = inferActor(root);
364
464
  const map = {
@@ -373,13 +473,26 @@ function buildIdContext(root, config, context) {
373
473
  STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
374
474
  TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
375
475
  PREFIX: sanitizeTemplateValue(prefix, "COOP"),
476
+ NAME: name || "item",
477
+ NAME_SLUG: slugifyLower(name || "item"),
376
478
  RAND: randomToken()
377
479
  };
378
480
  for (const [key, value] of Object.entries(fields)) {
379
481
  if (!value || !value.trim()) continue;
380
- const upper = key.toUpperCase();
482
+ const tokenName = normalizeNamingTokenName(key);
483
+ const upper = tokenName.toUpperCase();
381
484
  if (Object.prototype.hasOwnProperty.call(map, upper)) continue;
382
- map[upper] = sanitizeTemplateValue(value);
485
+ const tokenConfig = config.idTokens[tokenName];
486
+ if (!tokenConfig) {
487
+ continue;
488
+ }
489
+ const normalizedValue = normalizeNamingTokenValue(value);
490
+ if (tokenConfig.values.length > 0 && !tokenConfig.values.includes(normalizedValue)) {
491
+ throw new Error(
492
+ `Invalid value '${value}' for naming token '${tokenName}'. Allowed values: ${tokenConfig.values.join(", ")}.`
493
+ );
494
+ }
495
+ map[upper] = normalizedValue;
383
496
  }
384
497
  return map;
385
498
  }
@@ -398,52 +511,137 @@ function replaceTemplateToken(token, contextMap) {
398
511
  if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
399
512
  if (upper === "STATUS") return contextMap.STATUS;
400
513
  if (upper === "PREFIX") return contextMap.PREFIX;
514
+ if (upper === "NAME") return contextMap.NAME;
515
+ if (upper === "NAME_SLUG") return contextMap.NAME_SLUG;
401
516
  const dynamic = contextMap[upper];
402
517
  if (dynamic) return dynamic;
403
518
  return sanitizeTemplateValue(upper);
404
519
  }
405
- function renderNamingTemplate(template, contextMap) {
520
+ function renderNamingTemplate(template, contextMap, entityType) {
406
521
  const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
407
522
  const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
408
- return sanitizeTemplateValue(replaced, "COOP-ID");
523
+ return normalizeEntityIdValue(entityType, replaced, "COOP-ID");
409
524
  }
410
525
  function defaultCoopAuthor(root) {
411
526
  const actor = inferActor(root);
412
527
  return actor.trim() || "unknown";
413
528
  }
529
+ function normalizeEntityId(entityType, value) {
530
+ const trimmed = value.trim();
531
+ if (!trimmed) {
532
+ throw new Error(`Invalid ${entityType} id. Provide a non-empty value.`);
533
+ }
534
+ return normalizeEntityIdValue(entityType, trimmed, entityType);
535
+ }
536
+ function namingTemplatesForRoot(root) {
537
+ return readCoopConfig(root).idNamingTemplates;
538
+ }
539
+ function namingTokensForRoot(root) {
540
+ return readCoopConfig(root).idTokens;
541
+ }
542
+ function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
543
+ const config = readCoopConfig(root);
544
+ const digest = crypto.createHash("sha256").update(`${config.projectId}:${entityType}:${primaryId}`).digest("hex").toLowerCase();
545
+ const existing = new Set(existingShortIds.map((value) => value.toLowerCase()));
546
+ for (let width = 12; width <= digest.length; width += 2) {
547
+ const candidate = digest.slice(0, width);
548
+ if (!existing.has(candidate)) {
549
+ return candidate;
550
+ }
551
+ }
552
+ throw new Error(`Unable to generate a unique short id for ${entityType} '${primaryId}'.`);
553
+ }
554
+ function extractDynamicTokenFlags(commandPath, knownValueOptions, knownBooleanOptions = [], argv = process.argv.slice(2)) {
555
+ const start = argv.findIndex((_, index) => commandPath.every((segment, offset) => argv[index + offset] === segment));
556
+ if (start < 0) {
557
+ return {};
558
+ }
559
+ const result = {};
560
+ const valueOptions = new Set(knownValueOptions);
561
+ const booleanOptions = new Set(knownBooleanOptions);
562
+ for (let index = start + commandPath.length; index < argv.length; index += 1) {
563
+ const token = argv[index] ?? "";
564
+ if (!token.startsWith("--")) {
565
+ continue;
566
+ }
567
+ const [rawName, inlineValue] = token.slice(2).split("=", 2);
568
+ const name = rawName.trim();
569
+ if (!name) {
570
+ continue;
571
+ }
572
+ if (valueOptions.has(name)) {
573
+ if (inlineValue === void 0) {
574
+ index += 1;
575
+ }
576
+ continue;
577
+ }
578
+ if (booleanOptions.has(name) || name.startsWith("no-")) {
579
+ continue;
580
+ }
581
+ const value = inlineValue ?? argv[index + 1];
582
+ if (!value || value.startsWith("-")) {
583
+ throw new Error(`Dynamic naming flag '--${name}' requires a value.`);
584
+ }
585
+ result[name] = value;
586
+ if (inlineValue === void 0) {
587
+ index += 1;
588
+ }
589
+ }
590
+ return result;
591
+ }
414
592
  function previewNamingTemplate(template, context, root = process.cwd()) {
415
593
  const config = readCoopConfig(root);
594
+ const usedTemplate = template.trim().length > 0 ? template : config.idNamingTemplates[context.entityType];
595
+ const referencedTokens = extractTemplateTokens(usedTemplate);
596
+ for (const token of referencedTokens) {
597
+ if (!BUILT_IN_NAMING_TOKENS.has(token) && !config.idTokens[token.toLowerCase()]) {
598
+ throw new Error(`Naming template references unknown token <${token}>.`);
599
+ }
600
+ }
416
601
  const contextMap = buildIdContext(root, config, context);
417
- const rendered = renderNamingTemplate(template, {
602
+ const rendered = renderNamingTemplate(usedTemplate, {
418
603
  ...contextMap,
419
604
  RAND: "AB12CD34",
420
605
  YYMMDD: "260320",
421
606
  USER: "PKVSI"
422
- });
607
+ }, context.entityType);
423
608
  return rendered.replace(SEQ_MARKER, "1");
424
609
  }
425
610
  function generateConfiguredId(root, existingIds, context) {
426
611
  const config = readCoopConfig(root);
612
+ const template = config.idNamingTemplates[context.entityType];
613
+ const referencedTokens = extractTemplateTokens(template);
614
+ for (const token of referencedTokens) {
615
+ if (BUILT_IN_NAMING_TOKENS.has(token)) {
616
+ continue;
617
+ }
618
+ if (!config.idTokens[token.toLowerCase()]) {
619
+ throw new Error(`Naming template references unknown token <${token}>.`);
620
+ }
621
+ if (!context.fields || !Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === token.toLowerCase())) {
622
+ throw new Error(`Naming template requires token <${token}>. Pass --${token.toLowerCase()} <value>.`);
623
+ }
624
+ }
427
625
  const contextMap = buildIdContext(root, config, context);
428
626
  const existing = existingIds.map((id) => id.toUpperCase());
429
627
  for (let attempt = 0; attempt < 32; attempt += 1) {
430
- const rendered = renderNamingTemplate(config.idNamingTemplate, {
628
+ const rendered = renderNamingTemplate(template, {
431
629
  ...contextMap,
432
630
  RAND: randomToken()
433
- });
631
+ }, context.entityType);
434
632
  const seqMarker = SEQ_MARKER;
435
- const upperRendered = rendered.toUpperCase();
436
- if (!upperRendered.includes(seqMarker)) {
437
- if (!existing.includes(upperRendered)) {
438
- return upperRendered;
633
+ const normalizedRendered = context.entityType === "track" || context.entityType === "delivery" ? rendered : rendered.toUpperCase();
634
+ if (!normalizedRendered.includes(seqMarker)) {
635
+ if (!existing.includes(normalizedRendered.toUpperCase())) {
636
+ return normalizedRendered;
439
637
  }
440
638
  continue;
441
639
  }
442
- const [prefix, suffix] = upperRendered.split(seqMarker);
640
+ const [prefix, suffix] = normalizedRendered.split(seqMarker);
443
641
  const nextSeq = sequenceForPattern(existing, prefix ?? "", suffix ?? "");
444
642
  const seq = padSequence(nextSeq, config.idSeqPadding);
445
643
  const candidate = `${prefix ?? ""}${seq}${suffix ?? ""}`.replace(/-+/g, "-").replace(/^-+|-+$/g, "");
446
- if (!existing.includes(candidate)) {
644
+ if (!existing.includes(candidate.toUpperCase())) {
447
645
  return candidate;
448
646
  }
449
647
  }
@@ -551,32 +749,58 @@ function rebuildAliasIndex(root) {
551
749
  ensureCoopInitialized(root);
552
750
  const items = {};
553
751
  const aliases = {};
752
+ const shortIds = {};
554
753
  const ids = /* @__PURE__ */ new Set();
555
754
  const taskFiles = listTaskFiles(root);
755
+ const usedTaskShortIds = taskFiles.map((filePath) => parseTaskFile2(filePath)).map((parsed) => parsed.task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
556
756
  for (const filePath of taskFiles) {
557
757
  const parsed = parseTaskFile2(filePath);
558
758
  const id = parsed.task.id.toUpperCase();
559
759
  const source = path2.relative(root, filePath);
760
+ const shortId = parsed.task.short_id?.trim() ? parsed.task.short_id.trim().toLowerCase() : generateStableShortId(root, "task", parsed.task.id, usedTaskShortIds);
761
+ if (!parsed.task.short_id?.trim()) {
762
+ usedTaskShortIds.push(shortId);
763
+ const nextRaw = { ...parsed.raw, short_id: shortId };
764
+ const nextTask = { ...parsed.task, short_id: shortId };
765
+ writeTask(nextTask, { body: parsed.body, raw: nextRaw, filePath });
766
+ }
560
767
  ids.add(id);
561
768
  items[id] = {
562
769
  type: "task",
563
770
  aliases: parseAliases(parsed.raw, source),
564
- file: toPosixPath(path2.relative(root, filePath))
771
+ file: toPosixPath(path2.relative(root, filePath)),
772
+ short_id: shortId
565
773
  };
566
774
  }
567
775
  const ideaFiles = listIdeaFiles(root);
776
+ const usedIdeaShortIds = ideaFiles.map((filePath) => parseIdeaFile(filePath)).map((parsed) => parsed.idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
568
777
  for (const filePath of ideaFiles) {
569
778
  const parsed = parseIdeaFile(filePath);
570
779
  const id = parsed.idea.id.toUpperCase();
571
780
  const source = path2.relative(root, filePath);
781
+ const shortId = parsed.idea.short_id?.trim() ? parsed.idea.short_id.trim().toLowerCase() : generateStableShortId(root, "idea", parsed.idea.id, usedIdeaShortIds);
782
+ if (!parsed.idea.short_id?.trim()) {
783
+ usedIdeaShortIds.push(shortId);
784
+ const nextRaw = { ...parsed.raw, short_id: shortId };
785
+ const output2 = stringifyFrontmatter(nextRaw, parsed.body);
786
+ fs2.writeFileSync(filePath, output2, "utf8");
787
+ }
572
788
  ids.add(id);
573
789
  items[id] = {
574
790
  type: "idea",
575
791
  aliases: parseAliases(parsed.raw, source),
576
- file: toPosixPath(path2.relative(root, filePath))
792
+ file: toPosixPath(path2.relative(root, filePath)),
793
+ short_id: shortId
577
794
  };
578
795
  }
579
796
  for (const [id, item] of Object.entries(items)) {
797
+ if (item.short_id) {
798
+ const existingShort = shortIds[item.short_id];
799
+ if (existingShort && existingShort.id !== id) {
800
+ throw new Error(`Short id '${item.short_id}' is already mapped to '${existingShort.id}'.`);
801
+ }
802
+ shortIds[item.short_id] = { id, type: item.type, file: item.file, short_id: item.short_id };
803
+ }
580
804
  for (const alias of item.aliases) {
581
805
  if (ids.has(alias)) {
582
806
  throw new Error(`Alias '${alias}' conflicts with existing item id '${alias}'.`);
@@ -592,6 +816,7 @@ function rebuildAliasIndex(root) {
592
816
  version: 1,
593
817
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
594
818
  aliases,
819
+ short_ids: shortIds,
595
820
  items
596
821
  };
597
822
  writeIndexFile(root, data);
@@ -604,7 +829,7 @@ function loadAliasIndex(root) {
604
829
  return rebuildAliasIndex(root);
605
830
  }
606
831
  const parsed = readIndexFile(root, aliasIndexPath);
607
- if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items) {
832
+ if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items || !parsed.short_ids) {
608
833
  return rebuildAliasIndex(root);
609
834
  }
610
835
  return parsed;
@@ -619,6 +844,27 @@ function resolveReference(root, idOrAlias, expectedType) {
619
844
  }
620
845
  return { id: idCandidate, type: item.type, file: item.file };
621
846
  }
847
+ const shortCandidate = idOrAlias.trim().toLowerCase();
848
+ const shortMatch = index.short_ids[shortCandidate];
849
+ if (shortMatch) {
850
+ if (expectedType && shortMatch.type !== expectedType) {
851
+ throw new Error(`'${idOrAlias}' resolves to ${shortMatch.type} '${shortMatch.id}', expected ${expectedType}.`);
852
+ }
853
+ return shortMatch;
854
+ }
855
+ if (shortCandidate.length >= 6) {
856
+ const prefixMatches = Object.entries(index.short_ids).filter(([shortId]) => shortId.startsWith(shortCandidate)).map(([, target]) => target);
857
+ if (prefixMatches.length === 1) {
858
+ const match2 = prefixMatches[0];
859
+ if (expectedType && match2.type !== expectedType) {
860
+ throw new Error(`'${idOrAlias}' resolves to ${match2.type} '${match2.id}', expected ${expectedType}.`);
861
+ }
862
+ return match2;
863
+ }
864
+ if (prefixMatches.length > 1) {
865
+ throw new Error(`Ambiguous short id '${idOrAlias}'. Candidates: ${prefixMatches.map((entry) => entry.id).join(", ")}.`);
866
+ }
867
+ }
622
868
  const alias = normalizeAlias(idOrAlias);
623
869
  const match = index.aliases[alias];
624
870
  if (!match) {
@@ -725,17 +971,28 @@ function removeAliases(root, idOrAlias, values) {
725
971
  }
726
972
 
727
973
  // src/utils/table.ts
974
+ var ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
975
+ function visibleLength(value) {
976
+ return value.replace(ANSI_PATTERN, "").length;
977
+ }
978
+ function padEndAnsi(value, width) {
979
+ const visible = visibleLength(value);
980
+ if (visible >= width) {
981
+ return value;
982
+ }
983
+ return `${value}${" ".repeat(width - visible)}`;
984
+ }
728
985
  function formatTable(headers, rows) {
729
986
  const table = [headers, ...rows];
730
987
  const widths = [];
731
988
  for (let col = 0; col < headers.length; col += 1) {
732
- let width = headers[col].length;
989
+ let width = visibleLength(headers[col] ?? "");
733
990
  for (const row of table) {
734
- width = Math.max(width, (row[col] ?? "").length);
991
+ width = Math.max(width, visibleLength(row[col] ?? ""));
735
992
  }
736
993
  widths.push(width + 2);
737
994
  }
738
- return table.map((row) => row.map((cell, index) => (cell ?? "").padEnd(widths[index])).join("").trimEnd()).join("\n");
995
+ return table.map((row) => row.map((cell, index) => padEndAnsi(cell ?? "", widths[index])).join("").trimEnd()).join("\n");
739
996
  }
740
997
 
741
998
  // src/commands/alias.ts
@@ -1398,38 +1655,101 @@ import {
1398
1655
  stringifyFrontmatter as stringifyFrontmatter2,
1399
1656
  validateStructural as validateStructural2,
1400
1657
  validateSemantic,
1658
+ writeYamlFile as writeYamlFile3,
1401
1659
  writeTask as writeTask3
1402
1660
  } 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());
1661
+ function writeTrackFile(filePath, track) {
1662
+ writeYamlFile3(filePath, track);
1663
+ }
1664
+ function writeDeliveryFile(filePath, delivery, raw, body) {
1665
+ const payload = { ...raw, ...delivery };
1666
+ if (body.trim().length > 0) {
1667
+ fs3.writeFileSync(filePath, stringifyFrontmatter2(payload, body), "utf8");
1668
+ return;
1669
+ }
1670
+ writeYamlFile3(filePath, payload);
1671
+ }
1672
+ function loadTrackEntries(root) {
1673
+ const rawEntries = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile2(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id));
1674
+ const used = rawEntries.map((entry) => entry.track.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1675
+ let mutated = false;
1676
+ for (const entry of rawEntries) {
1677
+ if (entry.track.short_id?.trim()) {
1678
+ continue;
1409
1679
  }
1680
+ entry.track.short_id = generateStableShortId(root, "track", entry.track.id, used);
1681
+ used.push(entry.track.short_id);
1682
+ writeTrackFile(entry.filePath, entry.track);
1683
+ mutated = true;
1410
1684
  }
1411
- return map;
1685
+ return mutated ? rawEntries.map((entry) => ({ track: parseYamlFile2(entry.filePath), filePath: entry.filePath })) : rawEntries;
1412
1686
  }
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());
1687
+ function loadDeliveryEntries(root) {
1688
+ const rawEntries = listDeliveryFiles(root).map((filePath) => {
1689
+ const parsed = parseDeliveryFile(filePath);
1690
+ return { delivery: parsed.delivery, filePath, body: parsed.body, raw: parsed.raw };
1691
+ }).sort((a, b) => a.delivery.id.localeCompare(b.delivery.id));
1692
+ const used = rawEntries.map((entry) => entry.delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
1693
+ let mutated = false;
1694
+ for (const entry of rawEntries) {
1695
+ if (entry.delivery.short_id?.trim()) {
1696
+ continue;
1419
1697
  }
1698
+ entry.delivery.short_id = generateStableShortId(root, "delivery", entry.delivery.id, used);
1699
+ used.push(entry.delivery.short_id);
1700
+ writeDeliveryFile(entry.filePath, entry.delivery, entry.raw, entry.body);
1701
+ mutated = true;
1420
1702
  }
1421
- return map;
1703
+ return mutated ? rawEntries.map((entry) => {
1704
+ const parsed = parseDeliveryFile(entry.filePath);
1705
+ return { delivery: parsed.delivery, filePath: entry.filePath, body: parsed.body, raw: parsed.raw };
1706
+ }) : rawEntries;
1707
+ }
1708
+ function resolveUniqueShortId(entries, value) {
1709
+ const normalized = value.trim().toLowerCase();
1710
+ if (!normalized) return void 0;
1711
+ const exact = entries.find((entry) => entry.short_id?.toLowerCase() === normalized);
1712
+ if (exact) return exact;
1713
+ if (normalized.length < 6) return void 0;
1714
+ const matches = entries.filter((entry) => entry.short_id?.toLowerCase().startsWith(normalized));
1715
+ if (matches.length === 1) {
1716
+ return matches[0];
1717
+ }
1718
+ if (matches.length > 1) {
1719
+ throw new Error(`Ambiguous short id '${value}'. Candidates: ${matches.map((entry) => entry.id).join(", ")}.`);
1720
+ }
1721
+ return void 0;
1422
1722
  }
1423
1723
  function resolveExistingTrackId(root, trackId) {
1424
1724
  const normalized = trackId.trim().toLowerCase();
1425
1725
  if (!normalized) return void 0;
1426
1726
  if (normalized === "unassigned") return "unassigned";
1427
- return existingTrackMap(root).get(normalized);
1727
+ const entries = loadTrackEntries(root);
1728
+ const byId = entries.find((entry) => entry.track.id.trim().toLowerCase() === normalized);
1729
+ if (byId) return byId.track.id.trim();
1730
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.track), normalized);
1731
+ if (byShort) return byShort.id.trim();
1732
+ const byName = entries.filter((entry) => entry.track.name.trim().toLowerCase() === normalized);
1733
+ if (byName.length === 1) return byName[0].track.id.trim();
1734
+ if (byName.length > 1) {
1735
+ throw new Error(`Multiple tracks match '${trackId}'. Use the track id or short id.`);
1736
+ }
1737
+ return void 0;
1428
1738
  }
1429
1739
  function resolveExistingDeliveryId(root, deliveryId) {
1430
1740
  const normalized = deliveryId.trim().toLowerCase();
1431
1741
  if (!normalized) return void 0;
1432
- return existingDeliveryMap(root).get(normalized);
1742
+ const entries = loadDeliveryEntries(root);
1743
+ const byId = entries.find((entry) => entry.delivery.id.trim().toLowerCase() === normalized);
1744
+ if (byId) return byId.delivery.id.trim();
1745
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), normalized);
1746
+ if (byShort) return byShort.id.trim();
1747
+ const byName = entries.filter((entry) => entry.delivery.name.trim().toLowerCase() === normalized);
1748
+ if (byName.length === 1) return byName[0].delivery.id.trim();
1749
+ if (byName.length > 1) {
1750
+ throw new Error(`Multiple deliveries match '${deliveryId}'. Use the delivery id or short id.`);
1751
+ }
1752
+ return void 0;
1433
1753
  }
1434
1754
  function assertExistingTrackId(root, trackId) {
1435
1755
  const resolved = resolveExistingTrackId(root, trackId);
@@ -1480,11 +1800,27 @@ function resolveIdeaFile(root, idOrAlias) {
1480
1800
  }
1481
1801
  function loadTaskEntry(root, idOrAlias) {
1482
1802
  const filePath = resolveTaskFile(root, idOrAlias);
1483
- return { filePath, parsed: parseTaskFile4(filePath) };
1803
+ const parsed = parseTaskFile4(filePath);
1804
+ if (!parsed.task.short_id?.trim()) {
1805
+ const shortId = generateStableShortId(root, "task", parsed.task.id);
1806
+ const nextTask = { ...parsed.task, short_id: shortId };
1807
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1808
+ writeTask3(nextTask, { body: parsed.body, raw: nextRaw, filePath });
1809
+ return { filePath, parsed: parseTaskFile4(filePath) };
1810
+ }
1811
+ return { filePath, parsed };
1484
1812
  }
1485
1813
  function loadIdeaEntry(root, idOrAlias) {
1486
1814
  const filePath = resolveIdeaFile(root, idOrAlias);
1487
- return { filePath, parsed: parseIdeaFile2(filePath) };
1815
+ const parsed = parseIdeaFile2(filePath);
1816
+ if (!parsed.idea.short_id?.trim()) {
1817
+ const shortId = generateStableShortId(root, "idea", parsed.idea.id);
1818
+ const nextRaw = { ...parsed.raw, short_id: shortId };
1819
+ const output2 = stringifyFrontmatter2(nextRaw, parsed.body);
1820
+ fs3.writeFileSync(filePath, output2, "utf8");
1821
+ return { filePath, parsed: parseIdeaFile2(filePath) };
1822
+ }
1823
+ return { filePath, parsed };
1488
1824
  }
1489
1825
  function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
1490
1826
  const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
@@ -1566,18 +1902,18 @@ function taskEffectivePriority(task, track) {
1566
1902
  return effective_priority(task, track);
1567
1903
  }
1568
1904
  function resolveDeliveryEntry(root, ref) {
1569
- const files = listDeliveryFiles(root);
1570
1905
  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
- });
1906
+ const entries = loadDeliveryEntries(root);
1575
1907
  const direct = entries.find((entry) => entry.delivery.id.toLowerCase() === target);
1576
1908
  if (direct) return direct;
1909
+ const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), target);
1910
+ if (byShort) {
1911
+ return entries.find((entry) => entry.delivery.id === byShort.id);
1912
+ }
1577
1913
  const byName = entries.filter((entry) => entry.delivery.name.toLowerCase() === target);
1578
1914
  if (byName.length === 1) return byName[0];
1579
1915
  if (byName.length > 1) {
1580
- throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id.`);
1916
+ throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id or short id.`);
1581
1917
  }
1582
1918
  throw new Error(`Delivery '${ref}' not found.`);
1583
1919
  }
@@ -1716,6 +2052,7 @@ function selectTopReadyTask(root = resolveRepoRoot(), options = {}) {
1716
2052
  function formatSelectedTask(entry, selection = {}) {
1717
2053
  const lines = [
1718
2054
  `Selected task: ${entry.task.id}`,
2055
+ `Short ID: ${entry.task.short_id ?? "-"}`,
1719
2056
  `Title: ${entry.task.title}`,
1720
2057
  `Priority: ${selection.track && entry.task.priority_context?.[selection.track] ? `${entry.task.priority ?? "-"} -> ${entry.task.priority_context[selection.track]}` : entry.task.priority ?? "-"}`,
1721
2058
  `Track: ${entry.task.track ?? "-"}`,
@@ -2038,7 +2375,9 @@ function writeIdNamingValue(root, value) {
2038
2375
  const config = readCoopConfig(root).raw;
2039
2376
  const next = { ...config };
2040
2377
  const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
2041
- idRaw.naming = nextValue;
2378
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
2379
+ namingRaw.task = nextValue;
2380
+ idRaw.naming = namingRaw;
2042
2381
  next.id = idRaw;
2043
2382
  writeCoopConfig(root, next);
2044
2383
  }
@@ -2209,8 +2548,10 @@ import {
2209
2548
  TaskType as TaskType2,
2210
2549
  check_permission as check_permission2,
2211
2550
  load_auth_config as load_auth_config2,
2551
+ parseDeliveryFile as parseDeliveryFile2,
2212
2552
  parseTaskFile as parseTaskFile7,
2213
- writeYamlFile as writeYamlFile3,
2553
+ parseYamlFile as parseYamlFile3,
2554
+ writeYamlFile as writeYamlFile4,
2214
2555
  stringifyFrontmatter as stringifyFrontmatter4,
2215
2556
  writeTask as writeTask6
2216
2557
  } from "@kitsy/coop-core";
@@ -2713,6 +3054,41 @@ function plusDaysIso(days) {
2713
3054
  function unique2(values) {
2714
3055
  return Array.from(new Set(values));
2715
3056
  }
3057
+ function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
3058
+ const normalizedName = candidateName.trim().toLowerCase();
3059
+ if (!normalizedName) {
3060
+ return;
3061
+ }
3062
+ const conflict = entries.find(
3063
+ (entry) => entry.id.trim().toLowerCase() !== candidateId.trim().toLowerCase() && entry.name.trim().toLowerCase() === normalizedName
3064
+ );
3065
+ if (!conflict) {
3066
+ return;
3067
+ }
3068
+ throw new Error(
3069
+ `${kind === "track" ? "Track" : "Delivery"} name '${candidateName}' conflicts with existing ${kind} '${conflict.id}' (${conflict.name}). Names are matched case-insensitively.`
3070
+ );
3071
+ }
3072
+ function taskShortIds(root) {
3073
+ return listTaskFiles(root).map((filePath) => parseTaskFile7(filePath).task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3074
+ }
3075
+ function ideaShortIds(root) {
3076
+ return listIdeaFiles(root).map((filePath) => parseIdeaFile3(filePath).idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3077
+ }
3078
+ function trackShortIds(root) {
3079
+ return listTrackFiles(root).map((filePath) => parseYamlFile3(filePath).short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3080
+ }
3081
+ function deliveryShortIds(root) {
3082
+ return listDeliveryFiles(root).map((filePath) => parseDeliveryFile2(filePath).delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
3083
+ }
3084
+ function assertKnownDynamicFields(root, fields) {
3085
+ const tokens = namingTokensForRoot(root);
3086
+ for (const key of Object.keys(fields)) {
3087
+ if (!tokens[key]) {
3088
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
3089
+ }
3090
+ }
3091
+ }
2716
3092
  function resolveIdeaFile2(root, idOrAlias) {
2717
3093
  const target = resolveReference(root, idOrAlias, "idea");
2718
3094
  return path8.join(root, ...target.file.split("/"));
@@ -2741,10 +3117,33 @@ function makeTaskDraft(input2) {
2741
3117
  }
2742
3118
  function registerCreateCommand(program) {
2743
3119
  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) => {
3120
+ create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
2745
3121
  const root = resolveRepoRoot();
2746
3122
  const coop = ensureCoopInitialized(root);
2747
3123
  const interactive = Boolean(options.interactive);
3124
+ const dynamicFields = extractDynamicTokenFlags(
3125
+ ["create", "task"],
3126
+ [
3127
+ "id",
3128
+ "from",
3129
+ "title",
3130
+ "type",
3131
+ "status",
3132
+ "track",
3133
+ "delivery",
3134
+ "priority",
3135
+ "body",
3136
+ "acceptance",
3137
+ "tests-required",
3138
+ "authority-ref",
3139
+ "derived-ref",
3140
+ "from-file",
3141
+ "stdin",
3142
+ "interactive",
3143
+ "ai"
3144
+ ]
3145
+ );
3146
+ assertKnownDynamicFields(root, dynamicFields);
2748
3147
  if (options.fromFile?.trim() || options.stdin) {
2749
3148
  if (options.id || options.from || options.ai || options.title || titleArg || options.type || options.status || options.track || options.delivery || options.priority || options.body || (options.acceptance?.length ?? 0) > 0 || (options.testsRequired?.length ?? 0) > 0 || (options.authorityRef?.length ?? 0) > 0 || (options.derivedRef?.length ?? 0) > 0) {
2750
3149
  throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
@@ -2854,23 +3253,28 @@ function registerCreateCommand(program) {
2854
3253
  }
2855
3254
  const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
2856
3255
  const createdIds = [];
3256
+ const existingShortIds = taskShortIds(root);
3257
+ const createdShortIds = [];
2857
3258
  for (let index = 0; index < drafts.length; index += 1) {
2858
3259
  const draft = drafts[index];
2859
3260
  if (!draft) continue;
2860
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, [...existingIds, ...createdIds], {
3261
+ const id = (options.id?.trim() ? normalizeEntityId("task", options.id) : void 0) || generateConfiguredId(root, [...existingIds, ...createdIds], {
2861
3262
  entityType: "task",
2862
3263
  title: draft.title,
2863
3264
  taskType: draft.type,
2864
3265
  track: draft.track,
2865
3266
  status: draft.status,
3267
+ name: draft.title,
2866
3268
  fields: {
2867
3269
  track: draft.track,
2868
3270
  type: draft.type,
2869
- feature: draft.track || draft.type
3271
+ feature: draft.track || draft.type,
3272
+ ...dynamicFields
2870
3273
  }
2871
3274
  });
2872
3275
  const task = {
2873
3276
  id,
3277
+ short_id: generateStableShortId(root, "task", id, [...existingShortIds, ...createdShortIds]),
2874
3278
  title: draft.title,
2875
3279
  type: draft.type,
2876
3280
  status: draft.status,
@@ -2895,6 +3299,9 @@ function registerCreateCommand(program) {
2895
3299
  filePath
2896
3300
  });
2897
3301
  createdIds.push(id);
3302
+ if (normalizedTask.short_id) {
3303
+ createdShortIds.push(normalizedTask.short_id);
3304
+ }
2898
3305
  console.log(`Created task: ${path8.relative(root, filePath)}`);
2899
3306
  }
2900
3307
  if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
@@ -2908,10 +3315,15 @@ function registerCreateCommand(program) {
2908
3315
  console.log(`Linked ${createdIds.length} task(s) to idea: ${sourceIdeaParsed.idea.id}`);
2909
3316
  }
2910
3317
  });
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) => {
3318
+ create.command("idea").description("Create an idea").allowUnknownOption().allowExcessArguments().argument("[title]", "Idea title").option("--id <id>", "Idea id").option("--title <title>", "Idea title").option("--author <author>", "Idea author").option("--source <source>", "Idea source").option("--status <status>", `Idea status (${Object.values(IdeaStatus2).join(", ")})`).option("--tags <tags>", "Comma-separated tags").option("--body <body>", "Markdown body").option("--from-file <path>", "Create an idea from an idea draft file").option("--stdin", "Read idea draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
2912
3319
  const root = resolveRepoRoot();
2913
3320
  const coop = ensureCoopInitialized(root);
2914
3321
  const interactive = Boolean(options.interactive);
3322
+ const dynamicFields = extractDynamicTokenFlags(
3323
+ ["create", "idea"],
3324
+ ["id", "title", "author", "source", "status", "tags", "body", "from-file", "stdin", "interactive"]
3325
+ );
3326
+ assertKnownDynamicFields(root, dynamicFields);
2915
3327
  if (options.fromFile?.trim() || options.stdin) {
2916
3328
  if (options.id || options.title || titleArg || options.author || options.source || options.status || options.tags || options.body) {
2917
3329
  throw new Error("Cannot combine --from-file/--stdin with direct idea field flags. Use one input mode.");
@@ -2933,17 +3345,21 @@ function registerCreateCommand(program) {
2933
3345
  const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
2934
3346
  const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
2935
3347
  const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
2936
- const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
3348
+ const id = (options.id?.trim() ? normalizeEntityId("idea", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2937
3349
  entityType: "idea",
2938
3350
  title,
3351
+ name: title,
2939
3352
  status,
2940
3353
  fields: {
2941
3354
  source,
2942
- author
3355
+ author,
3356
+ ...dynamicFields
2943
3357
  }
2944
3358
  });
3359
+ const shortId = generateStableShortId(root, "idea", id, ideaShortIds(root));
2945
3360
  const frontmatter = {
2946
3361
  id,
3362
+ short_id: shortId,
2947
3363
  title,
2948
3364
  created: todayIsoDate(),
2949
3365
  aliases: [],
@@ -2960,10 +3376,15 @@ function registerCreateCommand(program) {
2960
3376
  fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
2961
3377
  console.log(`Created idea: ${path8.relative(root, filePath)}`);
2962
3378
  });
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) => {
3379
+ create.command("track").description("Create a track").allowUnknownOption().allowExcessArguments().argument("[name]", "Track name").option("--id <id>", "Track id").option("--name <name>", "Track name").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--max-wip <number>", "Max concurrent tasks").option("--allowed-types <types>", "Comma-separated allowed task types").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
2964
3380
  const root = resolveRepoRoot();
2965
3381
  const coop = ensureCoopInitialized(root);
2966
3382
  const interactive = Boolean(options.interactive);
3383
+ const dynamicFields = extractDynamicTokenFlags(
3384
+ ["create", "track"],
3385
+ ["id", "name", "profiles", "max-wip", "allowed-types", "interactive"]
3386
+ );
3387
+ assertKnownDynamicFields(root, dynamicFields);
2967
3388
  const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
2968
3389
  if (!name) throw new Error("Track name is required.");
2969
3390
  const capacityProfiles = unique2(
@@ -2985,19 +3406,21 @@ function registerCreateCommand(program) {
2985
3406
  }
2986
3407
  }
2987
3408
  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, {
3409
+ const id = (options.id?.trim() ? normalizeEntityId("track", options.id) : void 0) || generateConfiguredId(root, existingIds, {
2992
3410
  entityType: "track",
2993
- prefix,
2994
3411
  title: name,
3412
+ name,
2995
3413
  fields: {
2996
- name
3414
+ name,
3415
+ ...dynamicFields
2997
3416
  }
2998
3417
  });
3418
+ const existingTracks = listTrackFiles(root).map((filePath2) => parseYamlFile3(filePath2));
3419
+ assertNoCaseInsensitiveNameConflict("track", existingTracks, id, name);
3420
+ const shortId = generateStableShortId(root, "track", id, trackShortIds(root));
2999
3421
  const payload = {
3000
3422
  id,
3423
+ short_id: shortId,
3001
3424
  name,
3002
3425
  capacity_profiles: capacityProfiles,
3003
3426
  constraints: {
@@ -3006,13 +3429,32 @@ function registerCreateCommand(program) {
3006
3429
  }
3007
3430
  };
3008
3431
  const filePath = path8.join(coop, "tracks", `${id}.yml`);
3009
- writeYamlFile3(filePath, payload);
3432
+ writeYamlFile4(filePath, payload);
3010
3433
  console.log(`Created track: ${path8.relative(root, filePath)}`);
3011
3434
  });
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) => {
3435
+ create.command("delivery").description("Create a delivery").allowUnknownOption().allowExcessArguments().argument("[name]", "Delivery name").option("--id <id>", "Delivery id").option("--name <name>", "Delivery name").option("--status <status>", `Delivery status (${Object.values(DeliveryStatus).join(", ")})`).option("--commit", "Create delivery directly in committed state").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--budget-hours <hours>", "Engineering budget hours").option("--budget-cost <usd>", "Cost budget in USD").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--scope <ids>", "Comma-separated task ids to include in scope").option("--exclude <ids>", "Comma-separated task ids to exclude from scope").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
3013
3436
  const root = resolveRepoRoot();
3014
3437
  const coop = ensureCoopInitialized(root);
3015
3438
  const interactive = Boolean(options.interactive);
3439
+ const dynamicFields = extractDynamicTokenFlags(
3440
+ ["create", "delivery"],
3441
+ [
3442
+ "id",
3443
+ "name",
3444
+ "status",
3445
+ "commit",
3446
+ "target-date",
3447
+ "budget-hours",
3448
+ "budget-cost",
3449
+ "profiles",
3450
+ "scope",
3451
+ "exclude",
3452
+ "user",
3453
+ "force",
3454
+ "interactive"
3455
+ ]
3456
+ );
3457
+ assertKnownDynamicFields(root, dynamicFields);
3016
3458
  const user = options.user?.trim() || defaultCoopAuthor(root);
3017
3459
  const config = readCoopConfig(root);
3018
3460
  const auth = load_auth_config2(config.raw);
@@ -3088,21 +3530,22 @@ function registerCreateCommand(program) {
3088
3530
  throw new Error(`Scope exclude task '${id2}' does not exist.`);
3089
3531
  }
3090
3532
  }
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, {
3533
+ const existingIds = listDeliveryFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, ""));
3534
+ const id = (options.id?.trim() ? normalizeEntityId("delivery", options.id) : void 0) || generateConfiguredId(root, existingIds, {
3097
3535
  entityType: "delivery",
3098
- prefix,
3099
3536
  title: name,
3537
+ name,
3100
3538
  fields: {
3101
- status
3539
+ status,
3540
+ ...dynamicFields
3102
3541
  }
3103
3542
  });
3543
+ const existingDeliveries = listDeliveryFiles(root).map((filePath2) => parseDeliveryFile2(filePath2).delivery);
3544
+ assertNoCaseInsensitiveNameConflict("delivery", existingDeliveries, id, name);
3545
+ const shortId = generateStableShortId(root, "delivery", id, deliveryShortIds(root));
3104
3546
  const payload = {
3105
3547
  id,
3548
+ short_id: shortId,
3106
3549
  name,
3107
3550
  status,
3108
3551
  target_date: targetDate,
@@ -3119,7 +3562,7 @@ function registerCreateCommand(program) {
3119
3562
  }
3120
3563
  };
3121
3564
  const filePath = path8.join(coop, "deliveries", `${id}.yml`);
3122
- writeYamlFile3(filePath, payload);
3565
+ writeYamlFile4(filePath, payload);
3123
3566
  console.log(`Created delivery: ${path8.relative(root, filePath)}`);
3124
3567
  });
3125
3568
  }
@@ -3411,6 +3854,7 @@ var catalog = {
3411
3854
  "Use `coop project show` first to confirm the active workspace and project.",
3412
3855
  "Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
3413
3856
  "Before assigning `track` or `delivery` values to tasks, inspect or create named entities with `coop list tracks`, `coop list deliveries`, `coop create track`, and `coop create delivery`.",
3857
+ "Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
3414
3858
  "Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
3415
3859
  "Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
3416
3860
  "Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
@@ -3471,8 +3915,11 @@ var catalog = {
3471
3915
  { usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
3472
3916
  { usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
3473
3917
  { 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." }
3918
+ { usage: "coop naming", purpose: "Explain the effective per-entity naming rules, custom tokens, and examples." },
3919
+ { usage: 'coop naming preview "Natural-language COOP command recommender" --entity task', purpose: "Preview the generated ID before creating an item." },
3920
+ { usage: "coop naming set task <TYPE>-<TITLE16>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand." },
3921
+ { usage: "coop naming token create proj", purpose: "Create a custom naming token." },
3922
+ { usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." }
3476
3923
  ]
3477
3924
  },
3478
3925
  {
@@ -3483,8 +3930,10 @@ var catalog = {
3483
3930
  { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
3484
3931
  { usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
3485
3932
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3933
+ { usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
3934
+ { usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
3486
3935
  { 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." },
3936
+ { usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
3488
3937
  {
3489
3938
  usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
3490
3939
  purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
@@ -3628,9 +4077,10 @@ function renderTopicPayload(topic) {
3628
4077
  return {
3629
4078
  topic,
3630
4079
  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."
4080
+ "Use `coop naming` to inspect per-entity templates, custom tokens, and examples.",
4081
+ 'Use `coop naming preview "<title>" --entity <entity>` before creating a new item if predictable IDs matter.',
4082
+ "Use `coop naming set <entity> <template>` to update one entity's naming rule.",
4083
+ "Use `coop naming token create <token>` and `coop naming token value add <token> <value>` before passing custom token flags like `--proj UX`."
3634
4084
  ]
3635
4085
  };
3636
4086
  }
@@ -4358,7 +4808,13 @@ id_prefixes:
4358
4808
  run: "RUN"
4359
4809
 
4360
4810
  id:
4361
- naming: ${JSON.stringify(namingTemplate)}
4811
+ naming:
4812
+ task: ${JSON.stringify(namingTemplate)}
4813
+ idea: ${JSON.stringify(namingTemplate)}
4814
+ track: "<NAME_SLUG>"
4815
+ delivery: "<NAME_SLUG>"
4816
+ run: "<TYPE>-<YYMMDD>-<RAND>"
4817
+ tokens: {}
4362
4818
  seq_padding: 0
4363
4819
 
4364
4820
  defaults:
@@ -4720,15 +5176,14 @@ import path15 from "path";
4720
5176
  import {
4721
5177
  effective_priority as effective_priority3,
4722
5178
  load_graph as load_graph6,
4723
- parseDeliveryFile as parseDeliveryFile2,
4724
5179
  parseIdeaFile as parseIdeaFile4,
4725
5180
  parseTaskFile as parseTaskFile10,
4726
- parseYamlFile as parseYamlFile3,
4727
5181
  schedule_next as schedule_next3
4728
5182
  } from "@kitsy/coop-core";
4729
5183
  import chalk2 from "chalk";
4730
5184
  var TASK_COLUMN_WIDTHS = {
4731
5185
  id: 24,
5186
+ short: 12,
4732
5187
  title: 30,
4733
5188
  status: 12,
4734
5189
  priority: 4,
@@ -4740,6 +5195,7 @@ var TASK_COLUMN_WIDTHS = {
4740
5195
  };
4741
5196
  var IDEA_COLUMN_WIDTHS = {
4742
5197
  id: 24,
5198
+ short: 12,
4743
5199
  title: 30,
4744
5200
  status: 12,
4745
5201
  file: 30
@@ -4786,36 +5242,36 @@ function parseColumns(input2) {
4786
5242
  }
4787
5243
  function normalizeTaskColumns(value, ready) {
4788
5244
  if (!value?.trim()) {
4789
- return ready ? ["id", "title", "priority", "status", "assignee", "score"] : ["id", "title", "priority", "status", "assignee"];
5245
+ return ready ? ["id", "title", "priority", "status", "assignee", "track", "delivery", "score"] : ["id", "title", "priority", "status", "assignee", "track", "delivery"];
4790
5246
  }
4791
5247
  const raw = parseColumns(value);
4792
5248
  if (raw.length === 1 && raw[0] === "all") {
4793
- return ["id", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
5249
+ return ["id", "short", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
4794
5250
  }
4795
5251
  const normalized = raw.map((column) => {
4796
5252
  if (column === "p") return "priority";
4797
5253
  return column;
4798
5254
  });
4799
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
5255
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
4800
5256
  for (const column of normalized) {
4801
5257
  if (!valid.has(column)) {
4802
- throw new Error(`Invalid task column '${column}'. Expected id|title|status|priority|assignee|track|delivery|score|file|all.`);
5258
+ throw new Error(`Invalid task column '${column}'. Expected id|short|title|status|priority|assignee|track|delivery|score|file|all.`);
4803
5259
  }
4804
5260
  }
4805
5261
  return normalized;
4806
5262
  }
4807
5263
  function normalizeIdeaColumns(value) {
4808
5264
  if (!value?.trim()) {
4809
- return ["id", "title", "status"];
5265
+ return ["id", "short", "title", "status"];
4810
5266
  }
4811
5267
  const raw = parseColumns(value);
4812
5268
  if (raw.length === 1 && raw[0] === "all") {
4813
- return ["id", "title", "status", "file"];
5269
+ return ["id", "short", "title", "status", "file"];
4814
5270
  }
4815
- const valid = /* @__PURE__ */ new Set(["id", "title", "status", "file"]);
5271
+ const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "file"]);
4816
5272
  for (const column of raw) {
4817
5273
  if (!valid.has(column)) {
4818
- throw new Error(`Invalid idea column '${column}'. Expected id|title|status|file|all.`);
5274
+ throw new Error(`Invalid idea column '${column}'. Expected id|short|title|status|file|all.`);
4819
5275
  }
4820
5276
  }
4821
5277
  return raw;
@@ -4952,6 +5408,8 @@ function sortTaskRows(rows, sortMode, readyOrder, track) {
4952
5408
  }
4953
5409
  function taskColumnHeader(column) {
4954
5410
  switch (column) {
5411
+ case "short":
5412
+ return "Short";
4955
5413
  case "priority":
4956
5414
  return "P";
4957
5415
  case "assignee":
@@ -4975,6 +5433,8 @@ function taskColumnHeader(column) {
4975
5433
  }
4976
5434
  function ideaColumnHeader(column) {
4977
5435
  switch (column) {
5436
+ case "short":
5437
+ return "Short";
4978
5438
  case "file":
4979
5439
  return "File";
4980
5440
  case "status":
@@ -5051,6 +5511,7 @@ function listTasks(options) {
5051
5511
  }).map(({ task, filePath }) => ({
5052
5512
  task,
5053
5513
  id: task.id,
5514
+ shortId: task.short_id ?? "-",
5054
5515
  title: task.title,
5055
5516
  status: task.status,
5056
5517
  priority: taskEffectivePriority(task, resolvedTrack.value),
@@ -5071,10 +5532,12 @@ function listTasks(options) {
5071
5532
  (entry) => columns.map((column) => {
5072
5533
  const rawValue = (() => {
5073
5534
  switch (column) {
5535
+ case "short":
5536
+ return entry.shortId;
5074
5537
  case "title":
5075
5538
  return entry.title;
5076
5539
  case "status":
5077
- return statusColor(entry.status);
5540
+ return entry.status;
5078
5541
  case "priority":
5079
5542
  return entry.priority;
5080
5543
  case "assignee":
@@ -5092,7 +5555,8 @@ function listTasks(options) {
5092
5555
  return entry.id;
5093
5556
  }
5094
5557
  })();
5095
- return column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
5558
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
5559
+ return column === "status" ? statusColor(truncated) : truncated;
5096
5560
  })
5097
5561
  )
5098
5562
  )
@@ -5110,6 +5574,7 @@ function listIdeas(options) {
5110
5574
  return true;
5111
5575
  }).map(({ idea, filePath }) => ({
5112
5576
  id: idea.id,
5577
+ shortId: idea.short_id ?? "-",
5113
5578
  title: idea.title,
5114
5579
  status: idea.status,
5115
5580
  priority: "-",
@@ -5142,8 +5607,9 @@ function listIdeas(options) {
5142
5607
  columns.map(ideaColumnHeader),
5143
5608
  sorted.map(
5144
5609
  (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]);
5610
+ const rawValue = column === "short" ? entry.shortId : column === "title" ? entry.title : column === "status" ? entry.status : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
5611
+ const truncated = column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
5612
+ return column === "status" ? statusColor(truncated) : truncated;
5147
5613
  })
5148
5614
  )
5149
5615
  )
@@ -5153,10 +5619,11 @@ Total ideas: ${sorted.length}`);
5153
5619
  }
5154
5620
  function listDeliveries() {
5155
5621
  const root = resolveRepoRoot();
5156
- const rows = listDeliveryFiles(root).map((filePath) => ({ delivery: parseDeliveryFile2(filePath).delivery, filePath })).map(({ delivery, filePath }) => [
5622
+ const rows = loadDeliveryEntries(root).map(({ delivery, filePath }) => [
5157
5623
  truncateCell(delivery.id, 24),
5624
+ truncateCell(delivery.short_id ?? "-", 12),
5158
5625
  truncateCell(delivery.name, 30),
5159
- truncateCell(statusColor(delivery.status), 12),
5626
+ statusColor(truncateCell(delivery.status, 12)),
5160
5627
  truncateCell(delivery.target_date ?? "-", 16),
5161
5628
  truncateMiddleCell(path15.relative(root, filePath), 30)
5162
5629
  ]);
@@ -5164,12 +5631,13 @@ function listDeliveries() {
5164
5631
  console.log("No deliveries found.");
5165
5632
  return;
5166
5633
  }
5167
- console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
5634
+ console.log(formatTable(["ID", "Short", "Name", "Status", "Target", "File"], rows));
5168
5635
  }
5169
5636
  function listTracks() {
5170
5637
  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 }) => [
5638
+ const rows = loadTrackEntries(root).map(({ track, filePath }) => [
5172
5639
  truncateCell(track.id, 24),
5640
+ truncateCell(track.short_id ?? "-", 12),
5173
5641
  truncateCell(track.name, 30),
5174
5642
  truncateCell((track.capacity_profiles ?? []).join(", ") || "-", 24),
5175
5643
  truncateCell(String(track.constraints?.max_concurrent_tasks ?? "-"), 8),
@@ -5179,14 +5647,14 @@ function listTracks() {
5179
5647
  console.log("No tracks found.");
5180
5648
  return;
5181
5649
  }
5182
- console.log(formatTable(["ID", "Name", "Profiles", "Max WIP", "File"], rows));
5650
+ console.log(formatTable(["ID", "Short", "Name", "Profiles", "Max WIP", "File"], rows));
5183
5651
  }
5184
5652
  function registerListCommand(program) {
5185
5653
  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) => {
5654
+ list.command("tasks").alias("task").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,short,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
5187
5655
  listTasks(options);
5188
5656
  });
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) => {
5657
+ list.command("ideas").alias("idea").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,short,title,status,file or all").action((options) => {
5190
5658
  listIdeas(options);
5191
5659
  });
5192
5660
  list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
@@ -5202,18 +5670,21 @@ function registerListCommand(program) {
5202
5670
 
5203
5671
  // src/utils/logger.ts
5204
5672
  import fs11 from "fs";
5673
+ import os2 from "os";
5205
5674
  import path16 from "path";
5206
5675
  function resolveWorkspaceRoot(start = process.cwd()) {
5207
5676
  let current = path16.resolve(start);
5677
+ const configuredCoopHome = path16.resolve(resolveCoopHome());
5678
+ const defaultCoopHome = path16.resolve(path16.join(os2.homedir(), ".coop"));
5208
5679
  while (true) {
5209
5680
  const gitDir = path16.join(current, ".git");
5210
5681
  const coopDir2 = coopWorkspaceDir(current);
5211
5682
  const workspaceConfig = path16.join(coopDir2, "config.yml");
5212
- const projectsDir = path16.join(coopDir2, "projects");
5213
5683
  if (fs11.existsSync(gitDir)) {
5214
5684
  return current;
5215
5685
  }
5216
- if (fs11.existsSync(workspaceConfig) || fs11.existsSync(projectsDir)) {
5686
+ const resolvedCoopDir = path16.resolve(coopDir2);
5687
+ if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
5217
5688
  return current;
5218
5689
  }
5219
5690
  const parent = path16.dirname(current);
@@ -5340,7 +5811,7 @@ import fs12 from "fs";
5340
5811
  import path17 from "path";
5341
5812
  import { createInterface } from "readline/promises";
5342
5813
  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";
5814
+ import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile5 } from "@kitsy/coop-core";
5344
5815
  var COOP_IGNORE_TEMPLATE2 = `.index/
5345
5816
  logs/
5346
5817
  tmp/
@@ -5507,7 +5978,7 @@ async function migrateWorkspaceLayout(root, options) {
5507
5978
  }
5508
5979
  const movedConfigPath = path17.join(projectRoot, "config.yml");
5509
5980
  if (fs12.existsSync(movedConfigPath)) {
5510
- const movedConfig = parseYamlFile4(movedConfigPath);
5981
+ const movedConfig = parseYamlFile5(movedConfigPath);
5511
5982
  const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
5512
5983
  nextProject.name = identity.projectName;
5513
5984
  nextProject.id = projectId;
@@ -5515,7 +5986,7 @@ async function migrateWorkspaceLayout(root, options) {
5515
5986
  const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
5516
5987
  nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
5517
5988
  nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
5518
- writeYamlFile4(movedConfigPath, {
5989
+ writeYamlFile5(movedConfigPath, {
5519
5990
  ...movedConfig,
5520
5991
  project: nextProject,
5521
5992
  hooks: nextHooks
@@ -5569,25 +6040,55 @@ function registerMigrateCommand(program) {
5569
6040
  }
5570
6041
 
5571
6042
  // src/commands/naming.ts
6043
+ var ENTITY_NAMES = ["task", "idea", "track", "delivery", "run"];
6044
+ function assertNamingEntity(value) {
6045
+ const entity = value?.trim().toLowerCase() || "task";
6046
+ if (!ENTITY_NAMES.includes(entity)) {
6047
+ throw new Error(`Invalid entity '${value}'. Expected ${ENTITY_NAMES.join("|")}.`);
6048
+ }
6049
+ return entity;
6050
+ }
6051
+ function readConfigRecord(root) {
6052
+ return readCoopConfig(root).raw;
6053
+ }
6054
+ function writeNamingConfig(root, update) {
6055
+ writeCoopConfig(root, update(readConfigRecord(root)));
6056
+ }
5572
6057
  function printNamingOverview() {
5573
6058
  const root = resolveRepoRoot();
5574
6059
  const config = readCoopConfig(root);
6060
+ const templates = namingTemplatesForRoot(root);
6061
+ const tokens = namingTokensForRoot(root);
5575
6062
  const sampleTitle = "Natural-language COOP command recommender";
5576
6063
  const sampleTaskTitle = "Implement billing payment contract review";
5577
6064
  console.log("COOP Naming");
5578
- console.log(`Current template: ${config.idNamingTemplate}`);
5579
- console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
5580
- console.log("Tokens:");
6065
+ console.log(`Legacy task template: ${config.idNamingTemplate}`);
6066
+ console.log(`Default task template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
6067
+ console.log("Per-entity templates:");
6068
+ for (const entity of ENTITY_NAMES) {
6069
+ console.log(`- ${entity}: ${templates[entity]}`);
6070
+ }
6071
+ console.log("Built-in tokens:");
5581
6072
  console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
5582
6073
  console.log(" <TITLE> semantic title token (defaults to TITLE16)");
5583
6074
  console.log(" <TITLE16> semantic title token capped to 16 chars");
5584
6075
  console.log(" <TITLE24> semantic title token capped to 24 chars");
5585
6076
  console.log(" <TRACK> task track");
6077
+ console.log(" <NAME> entity name/title");
6078
+ console.log(" <NAME_SLUG> lower-case slug of the entity name");
5586
6079
  console.log(" <SEQ> sequential number within the rendered pattern");
5587
6080
  console.log(" <USER> actor/user namespace");
5588
6081
  console.log(" <YYMMDD> short date token");
5589
6082
  console.log(" <RAND> random uniqueness token");
5590
6083
  console.log(" <PREFIX> entity prefix override");
6084
+ console.log("Custom tokens:");
6085
+ if (Object.keys(tokens).length === 0) {
6086
+ console.log("- none");
6087
+ } else {
6088
+ for (const [token, definition] of Object.entries(tokens)) {
6089
+ console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6090
+ }
6091
+ }
5591
6092
  console.log("Examples:");
5592
6093
  const tokenExamples = namingTokenExamples(sampleTitle);
5593
6094
  for (const [token, value] of Object.entries(tokenExamples)) {
@@ -5597,7 +6098,7 @@ function printNamingOverview() {
5597
6098
  entityType: "idea",
5598
6099
  title: sampleTitle
5599
6100
  }, root)}`);
5600
- console.log(`Preview (${config.idNamingTemplate}): ${previewNamingTemplate(config.idNamingTemplate, {
6101
+ console.log(`Preview (${templates.task}): ${previewNamingTemplate(templates.task, {
5601
6102
  entityType: "task",
5602
6103
  title: sampleTaskTitle,
5603
6104
  track: "mvp",
@@ -5606,35 +6107,187 @@ function printNamingOverview() {
5606
6107
  }, root)}`);
5607
6108
  console.log("Try:");
5608
6109
  console.log(` coop naming preview "${sampleTitle}"`);
5609
- console.log(' coop config id.naming "<TYPE>-<TITLE24>"');
5610
- console.log(' coop config id.naming "<TRACK>-<SEQ>"');
6110
+ console.log(" coop naming set task <TYPE>-<TITLE24>-<SEQ>");
6111
+ console.log(" coop naming token create proj");
6112
+ console.log(" coop naming token value add proj UX");
6113
+ }
6114
+ function setEntityNamingTemplate(root, entity, template) {
6115
+ const nextTemplate = template.trim();
6116
+ if (!nextTemplate) {
6117
+ throw new Error("Naming template must be non-empty.");
6118
+ }
6119
+ writeNamingConfig(root, (config) => {
6120
+ const next = { ...config };
6121
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6122
+ const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
6123
+ namingRaw[entity] = nextTemplate;
6124
+ idRaw.naming = namingRaw;
6125
+ next.id = idRaw;
6126
+ return next;
6127
+ });
6128
+ }
6129
+ function ensureTokenRecord(config) {
6130
+ const next = { ...config };
6131
+ const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
6132
+ const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? { ...idRaw.tokens } : {};
6133
+ idRaw.tokens = tokensRaw;
6134
+ next.id = idRaw;
6135
+ return { next, tokens: tokensRaw };
6136
+ }
6137
+ function listTokens() {
6138
+ const root = resolveRepoRoot();
6139
+ const tokens = namingTokensForRoot(root);
6140
+ if (Object.keys(tokens).length === 0) {
6141
+ console.log("No naming tokens defined.");
6142
+ return;
6143
+ }
6144
+ for (const [token, definition] of Object.entries(tokens)) {
6145
+ console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
6146
+ }
5611
6147
  }
5612
6148
  function registerNamingCommand(program) {
5613
- const naming = program.command("naming").description("Explain COOP ID naming templates and preview examples");
6149
+ const naming = program.command("naming").description("Manage COOP naming templates, tokens, and previews");
5614
6150
  naming.action(() => {
5615
6151
  printNamingOverview();
5616
6152
  });
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(
6153
+ naming.command("preview").description("Preview the effective or supplied naming template for a sample title").allowUnknownOption().allowExcessArguments().argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
5618
6154
  (title, options) => {
5619
6155
  const root = resolveRepoRoot();
5620
- const config = readCoopConfig(root);
5621
- const entity = options.entity?.trim().toLowerCase() || "task";
5622
- const template = options.template?.trim() || config.idNamingTemplate;
6156
+ const entity = assertNamingEntity(options.entity);
6157
+ const templates = namingTemplatesForRoot(root);
6158
+ const dynamicFields = extractDynamicTokenFlags(
6159
+ ["naming", "preview"],
6160
+ ["template", "entity", "track", "status", "task-type"]
6161
+ );
6162
+ const tokens = namingTokensForRoot(root);
6163
+ for (const key of Object.keys(dynamicFields)) {
6164
+ if (!tokens[key]) {
6165
+ throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
6166
+ }
6167
+ }
5623
6168
  console.log(
5624
6169
  previewNamingTemplate(
5625
- template,
6170
+ options.template?.trim() || templates[entity],
5626
6171
  {
5627
6172
  entityType: entity,
5628
6173
  title,
6174
+ name: title,
5629
6175
  track: options.track,
5630
6176
  status: options.status,
5631
- taskType: options.taskType
6177
+ taskType: options.taskType,
6178
+ fields: dynamicFields
5632
6179
  },
5633
6180
  root
5634
6181
  )
5635
6182
  );
5636
6183
  }
5637
6184
  );
6185
+ naming.command("set").description("Set the naming template for one entity type").argument("<entity>", "Entity: task|idea|track|delivery|run").argument("<template>", "Naming template").action((entity, template) => {
6186
+ const root = resolveRepoRoot();
6187
+ setEntityNamingTemplate(root, assertNamingEntity(entity), template);
6188
+ console.log(`${assertNamingEntity(entity)}=${namingTemplatesForRoot(root)[assertNamingEntity(entity)]}`);
6189
+ });
6190
+ const token = naming.command("token").description("Manage custom naming tokens");
6191
+ token.command("list").description("List naming tokens").action(() => {
6192
+ listTokens();
6193
+ });
6194
+ token.command("create").description("Create a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6195
+ const root = resolveRepoRoot();
6196
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6197
+ writeNamingConfig(root, (config) => {
6198
+ const { next, tokens } = ensureTokenRecord(config);
6199
+ if (tokens[normalizedToken]) {
6200
+ throw new Error(`Naming token '${normalizedToken}' already exists.`);
6201
+ }
6202
+ tokens[normalizedToken] = { values: [] };
6203
+ return next;
6204
+ });
6205
+ console.log(`Created naming token: ${normalizedToken}`);
6206
+ });
6207
+ token.command("rename").description("Rename a custom naming token").argument("<token>", "Existing token name").argument("<next>", "New token name").action((tokenName, nextToken) => {
6208
+ const root = resolveRepoRoot();
6209
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6210
+ const normalizedNext = normalizeNamingTokenName(nextToken);
6211
+ const tokens = namingTokensForRoot(root);
6212
+ if (!tokens[normalizedToken]) {
6213
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6214
+ }
6215
+ if (tokens[normalizedNext]) {
6216
+ throw new Error(`Naming token '${normalizedNext}' already exists.`);
6217
+ }
6218
+ writeNamingConfig(root, (config) => {
6219
+ const { next, tokens: tokensRaw } = ensureTokenRecord(config);
6220
+ tokensRaw[normalizedNext] = tokensRaw[normalizedToken];
6221
+ delete tokensRaw[normalizedToken];
6222
+ return next;
6223
+ });
6224
+ console.log(`Renamed naming token: ${normalizedToken} -> ${normalizedNext}`);
6225
+ });
6226
+ token.command("delete").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
6227
+ const root = resolveRepoRoot();
6228
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6229
+ writeNamingConfig(root, (config) => {
6230
+ const { next, tokens } = ensureTokenRecord(config);
6231
+ delete tokens[normalizedToken];
6232
+ return next;
6233
+ });
6234
+ console.log(`Deleted naming token: ${normalizedToken}`);
6235
+ });
6236
+ const tokenValue = token.command("value").description("Manage allowed values for a naming token");
6237
+ tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
6238
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6239
+ const tokens = namingTokensForRoot(resolveRepoRoot());
6240
+ const values = tokens[normalizedToken]?.values;
6241
+ if (!values) {
6242
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6243
+ }
6244
+ if (values.length === 0) {
6245
+ console.log("(no values)");
6246
+ return;
6247
+ }
6248
+ for (const value of values) {
6249
+ console.log(value);
6250
+ }
6251
+ });
6252
+ tokenValue.command("add").description("Add an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6253
+ const root = resolveRepoRoot();
6254
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6255
+ const normalizedValue = normalizeNamingTokenValue(value);
6256
+ writeNamingConfig(root, (config) => {
6257
+ const { next, tokens } = ensureTokenRecord(config);
6258
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6259
+ if (!tokenRecord) {
6260
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6261
+ }
6262
+ const values = Array.isArray(tokenRecord.values) ? Array.from(
6263
+ new Set(
6264
+ tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
6265
+ )
6266
+ ) : [];
6267
+ values.push(normalizedValue);
6268
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6269
+ tokens[normalizedToken] = tokenRecord;
6270
+ return next;
6271
+ });
6272
+ console.log(`Added naming token value: ${normalizedToken}=${normalizedValue}`);
6273
+ });
6274
+ tokenValue.command("remove").description("Remove an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
6275
+ const root = resolveRepoRoot();
6276
+ const normalizedToken = normalizeNamingTokenName(tokenName);
6277
+ const normalizedValue = normalizeNamingTokenValue(value);
6278
+ writeNamingConfig(root, (config) => {
6279
+ const { next, tokens } = ensureTokenRecord(config);
6280
+ const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
6281
+ if (!tokenRecord) {
6282
+ throw new Error(`Naming token '${normalizedToken}' does not exist.`);
6283
+ }
6284
+ const values = Array.isArray(tokenRecord.values) ? tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry)).filter((entry) => entry !== normalizedValue) : [];
6285
+ tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
6286
+ tokens[normalizedToken] = tokenRecord;
6287
+ return next;
6288
+ });
6289
+ console.log(`Removed naming token value: ${normalizedToken}=${normalizedValue}`);
6290
+ });
5638
6291
  }
5639
6292
 
5640
6293
  // src/commands/plan.ts
@@ -5955,7 +6608,13 @@ id_prefixes:
5955
6608
  run: "RUN"
5956
6609
 
5957
6610
  id:
5958
- naming: ${JSON.stringify(namingTemplate)}
6611
+ naming:
6612
+ task: ${JSON.stringify(namingTemplate)}
6613
+ idea: ${JSON.stringify(namingTemplate)}
6614
+ track: "<NAME_SLUG>"
6615
+ delivery: "<NAME_SLUG>"
6616
+ run: "<TYPE>-<YYMMDD>-<RAND>"
6617
+ tokens: {}
5959
6618
  seq_padding: 0
5960
6619
 
5961
6620
  defaults:
@@ -6375,7 +7034,7 @@ function registerRunCommand(program) {
6375
7034
  }
6376
7035
 
6377
7036
  // src/commands/search.ts
6378
- import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile3, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
7037
+ import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile4, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
6379
7038
  function haystackForTask(task) {
6380
7039
  return [
6381
7040
  task.id,
@@ -6454,7 +7113,7 @@ ${parsed.body}`, query)) continue;
6454
7113
  }
6455
7114
  if (options.kind === "all" || options.kind === "delivery") {
6456
7115
  for (const filePath of listDeliveryFiles(root)) {
6457
- const parsed = parseDeliveryFile3(filePath);
7116
+ const parsed = parseDeliveryFile4(filePath);
6458
7117
  if (options.status && parsed.delivery.status !== options.status) continue;
6459
7118
  if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
6460
7119
  rows.push({
@@ -6798,6 +7457,7 @@ function showTask(taskId, options = {}) {
6798
7457
  if (options.compact) {
6799
7458
  const compactLines = [
6800
7459
  `Task: ${task.id}`,
7460
+ `Short ID: ${task.short_id ?? "-"}`,
6801
7461
  `Title: ${task.title}`,
6802
7462
  `Status: ${task.status}`,
6803
7463
  `Priority: ${task.priority ?? "-"}`,
@@ -6815,6 +7475,7 @@ function showTask(taskId, options = {}) {
6815
7475
  }
6816
7476
  const lines = [
6817
7477
  `Task: ${task.id}`,
7478
+ `Short ID: ${task.short_id ?? "-"}`,
6818
7479
  `Title: ${task.title}`,
6819
7480
  `Status: ${task.status}`,
6820
7481
  `Type: ${task.type}`,
@@ -6888,6 +7549,7 @@ function showIdea(ideaId, options = {}) {
6888
7549
  console.log(
6889
7550
  [
6890
7551
  `Idea: ${idea.id}`,
7552
+ `Short ID: ${idea.short_id ?? "-"}`,
6891
7553
  `Title: ${idea.title}`,
6892
7554
  `Status: ${idea.status}`,
6893
7555
  `Tags: ${stringify(idea.tags)}`,
@@ -6899,6 +7561,7 @@ function showIdea(ideaId, options = {}) {
6899
7561
  }
6900
7562
  const lines = [
6901
7563
  `Idea: ${idea.id}`,
7564
+ `Short ID: ${idea.short_id ?? "-"}`,
6902
7565
  `Title: ${idea.title}`,
6903
7566
  `Status: ${idea.status}`,
6904
7567
  `Author: ${idea.author}`,
@@ -6921,6 +7584,7 @@ function showDelivery(ref, options = {}) {
6921
7584
  console.log(
6922
7585
  [
6923
7586
  `Delivery: ${delivery.id}`,
7587
+ `Short ID: ${delivery.short_id ?? "-"}`,
6924
7588
  `Name: ${delivery.name}`,
6925
7589
  `Status: ${delivery.status}`,
6926
7590
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6932,6 +7596,7 @@ function showDelivery(ref, options = {}) {
6932
7596
  }
6933
7597
  const lines = [
6934
7598
  `Delivery: ${delivery.id}`,
7599
+ `Short ID: ${delivery.short_id ?? "-"}`,
6935
7600
  `Name: ${delivery.name}`,
6936
7601
  `Status: ${delivery.status}`,
6937
7602
  `Target Date: ${delivery.target_date ?? "-"}`,
@@ -6947,6 +7612,28 @@ function showDelivery(ref, options = {}) {
6947
7612
  ];
6948
7613
  console.log(lines.join("\n"));
6949
7614
  }
7615
+ function showTrack(ref, options = {}) {
7616
+ const root = resolveRepoRoot();
7617
+ const resolvedId = resolveExistingTrackId(root, ref);
7618
+ if (!resolvedId) {
7619
+ throw new Error(`Track '${ref}' not found.`);
7620
+ }
7621
+ const entry = loadTrackEntries(root).find((candidate) => candidate.track.id === resolvedId);
7622
+ if (!entry) {
7623
+ throw new Error(`Track '${ref}' not found.`);
7624
+ }
7625
+ const track = entry.track;
7626
+ const base = [
7627
+ `Track: ${track.id}`,
7628
+ `Short ID: ${track.short_id ?? "-"}`,
7629
+ `Name: ${track.name}`,
7630
+ `Profiles: ${(track.capacity_profiles ?? []).join(", ") || "-"}`,
7631
+ `Max WIP: ${track.constraints?.max_concurrent_tasks ?? "-"}`,
7632
+ `Allowed Types: ${(track.constraints?.allowed_types ?? []).join(", ") || "-"}`,
7633
+ `File: ${path22.relative(root, entry.filePath)}`
7634
+ ];
7635
+ console.log((options.compact ? base.filter((line) => !line.startsWith("Allowed Types")) : base).join("\n"));
7636
+ }
6950
7637
  function showByReference(ref, options = {}) {
6951
7638
  const root = resolveRepoRoot();
6952
7639
  try {
@@ -6958,7 +7645,11 @@ function showByReference(ref, options = {}) {
6958
7645
  showIdea(ref, options);
6959
7646
  return;
6960
7647
  } catch {
6961
- showDelivery(ref, options);
7648
+ try {
7649
+ showDelivery(ref, options);
7650
+ } catch {
7651
+ showTrack(ref, options);
7652
+ }
6962
7653
  }
6963
7654
  }
6964
7655
  function registerShowCommand(program) {
@@ -6974,6 +7665,9 @@ function registerShowCommand(program) {
6974
7665
  show.command("idea").description("Show idea details").argument("<id>", "Idea ID").option("--compact", "Show a smaller summary view").action((id, options) => {
6975
7666
  showIdea(id, options);
6976
7667
  });
7668
+ show.command("track").description("Show track details").argument("<id>", "Track id, short id, or unique name").option("--compact", "Show a smaller summary view").action((id, options) => {
7669
+ showTrack(id, options);
7670
+ });
6977
7671
  show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").option("--compact", "Show a smaller summary view").action((id, options) => {
6978
7672
  showDelivery(id, options);
6979
7673
  });
@@ -7556,6 +8250,10 @@ function registerUseCommand(program) {
7556
8250
  const root = resolveRepoRoot();
7557
8251
  printContext(clearWorkingContext(root, resolveCoopHome(), scope));
7558
8252
  });
8253
+ use.command("reset").description("Clear all working-context values").action(() => {
8254
+ const root = resolveRepoRoot();
8255
+ printContext(clearWorkingContext(root, resolveCoopHome(), "all"));
8256
+ });
7559
8257
  }
7560
8258
 
7561
8259
  // src/commands/view.ts
@@ -7896,7 +8594,7 @@ function registerWebhookCommand(program) {
7896
8594
 
7897
8595
  // src/merge-driver/merge-driver.ts
7898
8596
  import fs21 from "fs";
7899
- import os2 from "os";
8597
+ import os3 from "os";
7900
8598
  import path25 from "path";
7901
8599
  import { spawnSync as spawnSync5 } from "child_process";
7902
8600
  import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
@@ -7987,7 +8685,7 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
7987
8685
  const ours = parseTaskDocument(oursRaw, oursPath);
7988
8686
  const theirs = parseTaskDocument(theirsRaw, theirsPath);
7989
8687
  const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
7990
- const tempDir = fs21.mkdtempSync(path25.join(os2.tmpdir(), "coop-merge-body-"));
8688
+ const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
7991
8689
  try {
7992
8690
  const ancestorBody = path25.join(tempDir, "ancestor.md");
7993
8691
  const oursBody = path25.join(tempDir, "ours.md");
@@ -8035,6 +8733,8 @@ function renderBasicHelp() {
8035
8733
  "- `coop current`: show working context, active work, and the next ready task",
8036
8734
  "- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
8037
8735
  "- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
8736
+ "- `coop naming`: inspect per-entity ID rules and naming tokens",
8737
+ '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item',
8038
8738
  "- `coop next task` or `coop pick task`: choose work from COOP",
8039
8739
  "- `coop show <id>`: inspect a task, idea, or delivery",
8040
8740
  "- `coop list tasks --track <id>`: browse scoped work",
@@ -8082,6 +8782,7 @@ Common day-to-day commands:
8082
8782
  coop current
8083
8783
  coop next task
8084
8784
  coop show <id>
8785
+ coop naming
8085
8786
  `);
8086
8787
  registerInitCommand(program);
8087
8788
  registerCreateCommand(program);