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