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