@kitsy/coop 2.2.3 → 2.2.5
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 +314 -102
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -44,8 +44,6 @@ var BUILT_IN_NAMING_TOKENS = /* @__PURE__ */ new Set([
|
|
|
44
44
|
"TYPE",
|
|
45
45
|
"ENTITY",
|
|
46
46
|
"TITLE",
|
|
47
|
-
"TITLE16",
|
|
48
|
-
"TITLE24",
|
|
49
47
|
"TRACK",
|
|
50
48
|
"STATUS",
|
|
51
49
|
"TASK_TYPE",
|
|
@@ -57,6 +55,10 @@ var BUILT_IN_NAMING_TOKENS = /* @__PURE__ */ new Set([
|
|
|
57
55
|
"NAME",
|
|
58
56
|
"NAME_SLUG"
|
|
59
57
|
]);
|
|
58
|
+
function isBuiltInNamingToken(token) {
|
|
59
|
+
const upper = token.toUpperCase();
|
|
60
|
+
return BUILT_IN_NAMING_TOKENS.has(upper) || /^TITLE\d{1,2}$/.test(upper);
|
|
61
|
+
}
|
|
60
62
|
var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
61
63
|
"A",
|
|
62
64
|
"AN",
|
|
@@ -400,7 +402,7 @@ function semanticTitleToken(input2, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
|
|
|
400
402
|
const segments = [];
|
|
401
403
|
let used = 0;
|
|
402
404
|
for (const word of words) {
|
|
403
|
-
const remaining = safeMaxLength - used
|
|
405
|
+
const remaining = safeMaxLength - used;
|
|
404
406
|
if (remaining < 2) {
|
|
405
407
|
break;
|
|
406
408
|
}
|
|
@@ -409,7 +411,7 @@ function semanticTitleToken(input2, maxLength = DEFAULT_TITLE_TOKEN_LENGTH) {
|
|
|
409
411
|
continue;
|
|
410
412
|
}
|
|
411
413
|
segments.push(segment);
|
|
412
|
-
used += segment.length
|
|
414
|
+
used += segment.length;
|
|
413
415
|
}
|
|
414
416
|
if (segments.length > 0) {
|
|
415
417
|
return segments.join("-");
|
|
@@ -466,6 +468,7 @@ function buildIdContext(root, config, context) {
|
|
|
466
468
|
ENTITY: sanitizeTemplateValue(entityType, entityType),
|
|
467
469
|
USER: normalizeIdPart(actor, "USER", 16),
|
|
468
470
|
YYMMDD: shortDateToken(now),
|
|
471
|
+
TITLE_SOURCE: title,
|
|
469
472
|
TITLE: semanticTitleToken(title, DEFAULT_TITLE_TOKEN_LENGTH),
|
|
470
473
|
TITLE16: semanticTitleToken(title, 16),
|
|
471
474
|
TITLE24: semanticTitleToken(title, 24),
|
|
@@ -505,7 +508,11 @@ function replaceTemplateToken(token, contextMap) {
|
|
|
505
508
|
if (upper === "TYPE" || upper === "ENTITY") return contextMap.TYPE;
|
|
506
509
|
if (upper === "TITLE") return contextMap.TITLE;
|
|
507
510
|
if (/^TITLE\d+$/.test(upper)) {
|
|
508
|
-
|
|
511
|
+
const parsed = Number(upper.slice("TITLE".length));
|
|
512
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
513
|
+
return semanticTitleToken(contextMap.TITLE_SOURCE ?? "", parsed);
|
|
514
|
+
}
|
|
515
|
+
return contextMap.TITLE;
|
|
509
516
|
}
|
|
510
517
|
if (upper === "TRACK") return contextMap.TRACK;
|
|
511
518
|
if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
|
|
@@ -539,6 +546,11 @@ function namingTemplatesForRoot(root) {
|
|
|
539
546
|
function namingTokensForRoot(root) {
|
|
540
547
|
return readCoopConfig(root).idTokens;
|
|
541
548
|
}
|
|
549
|
+
function requiredCustomNamingTokens(root, entityType) {
|
|
550
|
+
const config = readCoopConfig(root);
|
|
551
|
+
const template = config.idNamingTemplates[entityType];
|
|
552
|
+
return extractTemplateTokens(template).filter((token) => !isBuiltInNamingToken(token)).map((token) => token.toLowerCase());
|
|
553
|
+
}
|
|
542
554
|
function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
|
|
543
555
|
const config = readCoopConfig(root);
|
|
544
556
|
const digest = crypto.createHash("sha256").update(`${config.projectId}:${entityType}:${primaryId}`).digest("hex").toLowerCase();
|
|
@@ -594,7 +606,7 @@ function previewNamingTemplate(template, context, root = process.cwd()) {
|
|
|
594
606
|
const usedTemplate = template.trim().length > 0 ? template : config.idNamingTemplates[context.entityType];
|
|
595
607
|
const referencedTokens = extractTemplateTokens(usedTemplate);
|
|
596
608
|
for (const token of referencedTokens) {
|
|
597
|
-
if (!
|
|
609
|
+
if (!isBuiltInNamingToken(token) && !config.idTokens[token.toLowerCase()]) {
|
|
598
610
|
throw new Error(`Naming template references unknown token <${token}>.`);
|
|
599
611
|
}
|
|
600
612
|
}
|
|
@@ -612,7 +624,7 @@ function generateConfiguredId(root, existingIds, context) {
|
|
|
612
624
|
const template = config.idNamingTemplates[context.entityType];
|
|
613
625
|
const referencedTokens = extractTemplateTokens(template);
|
|
614
626
|
for (const token of referencedTokens) {
|
|
615
|
-
if (
|
|
627
|
+
if (isBuiltInNamingToken(token)) {
|
|
616
628
|
continue;
|
|
617
629
|
}
|
|
618
630
|
if (!config.idTokens[token.toLowerCase()]) {
|
|
@@ -1037,7 +1049,7 @@ function registerAliasCommand(program) {
|
|
|
1037
1049
|
const suffix = result.added.length > 0 ? result.added.join(", ") : "no new aliases";
|
|
1038
1050
|
console.log(`Updated ${result.target.type} ${result.target.id}: ${suffix}`);
|
|
1039
1051
|
});
|
|
1040
|
-
alias.command("rm").description("Remove aliases from an item").argument("<idOrAlias>", "Target item id or alias").argument("<aliases...>", "Aliases to remove").action((idOrAlias, aliases) => {
|
|
1052
|
+
alias.command("rm").alias("remove").description("Remove aliases from an item").argument("<idOrAlias>", "Target item id or alias").argument("<aliases...>", "Aliases to remove").action((idOrAlias, aliases) => {
|
|
1041
1053
|
const root = resolveRepoRoot();
|
|
1042
1054
|
const result = removeAliases(root, idOrAlias, aliases);
|
|
1043
1055
|
const suffix = result.removed.length > 0 ? result.removed.join(", ") : "no aliases removed";
|
|
@@ -1876,6 +1888,8 @@ function promoteTaskForContext(task, context) {
|
|
|
1876
1888
|
const next = {
|
|
1877
1889
|
...task,
|
|
1878
1890
|
updated: todayIsoDate(),
|
|
1891
|
+
promoted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1892
|
+
promoted_track: context.track?.trim() || null,
|
|
1879
1893
|
fix_versions: [...task.fix_versions ?? []],
|
|
1880
1894
|
delivery_tracks: [...task.delivery_tracks ?? []],
|
|
1881
1895
|
priority_context: { ...task.priority_context ?? {} }
|
|
@@ -2660,6 +2674,12 @@ function asUniqueStrings(value) {
|
|
|
2660
2674
|
);
|
|
2661
2675
|
return entries.length > 0 ? entries : void 0;
|
|
2662
2676
|
}
|
|
2677
|
+
function formatIgnoredFieldWarning(source, context, keys) {
|
|
2678
|
+
if (keys.length === 0) {
|
|
2679
|
+
return "";
|
|
2680
|
+
}
|
|
2681
|
+
return `${source}: ignored unknown ${context} field(s): ${keys.sort((a, b) => a.localeCompare(b)).join(", ")}`;
|
|
2682
|
+
}
|
|
2663
2683
|
function parseIdeaDraftObject(record, source) {
|
|
2664
2684
|
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
2665
2685
|
if (!title) {
|
|
@@ -2669,25 +2689,34 @@ function parseIdeaDraftObject(record, source) {
|
|
|
2669
2689
|
if (status && !Object.values(IdeaStatus).includes(status)) {
|
|
2670
2690
|
throw new Error(`${source}: invalid idea status '${status}'.`);
|
|
2671
2691
|
}
|
|
2692
|
+
const allowedKeys = /* @__PURE__ */ new Set(["id", "title", "author", "source", "status", "tags", "aliases", "linked_tasks", "body"]);
|
|
2693
|
+
const ignoredKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
|
|
2672
2694
|
return {
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2695
|
+
value: {
|
|
2696
|
+
id: typeof record.id === "string" && record.id.trim() ? record.id.trim().toUpperCase() : void 0,
|
|
2697
|
+
title,
|
|
2698
|
+
author: typeof record.author === "string" && record.author.trim() ? record.author.trim() : void 0,
|
|
2699
|
+
source: typeof record.source === "string" && record.source.trim() ? record.source.trim() : void 0,
|
|
2700
|
+
status: status ? status : void 0,
|
|
2701
|
+
tags: asUniqueStrings(record.tags),
|
|
2702
|
+
aliases: asUniqueStrings(record.aliases),
|
|
2703
|
+
linked_tasks: asUniqueStrings(record.linked_tasks),
|
|
2704
|
+
body: typeof record.body === "string" ? record.body : void 0
|
|
2705
|
+
},
|
|
2706
|
+
warnings: ignoredKeys.length > 0 ? [formatIgnoredFieldWarning(source, "idea draft", ignoredKeys)] : []
|
|
2682
2707
|
};
|
|
2683
2708
|
}
|
|
2684
|
-
function
|
|
2709
|
+
function parseIdeaDraftInputWithWarnings(content, source) {
|
|
2685
2710
|
const trimmed = content.trimStart();
|
|
2686
2711
|
if (trimmed.startsWith("---")) {
|
|
2687
2712
|
const { frontmatter, body } = parseFrontmatterContent(content, source);
|
|
2713
|
+
const parsed = parseIdeaDraftObject(frontmatter, source);
|
|
2688
2714
|
return {
|
|
2689
|
-
|
|
2690
|
-
|
|
2715
|
+
value: {
|
|
2716
|
+
...parsed.value,
|
|
2717
|
+
body: body || (typeof frontmatter.body === "string" ? frontmatter.body : void 0)
|
|
2718
|
+
},
|
|
2719
|
+
warnings: parsed.warnings
|
|
2691
2720
|
};
|
|
2692
2721
|
}
|
|
2693
2722
|
return parseIdeaDraftObject(parseYamlContent(content, source), source);
|
|
@@ -2758,6 +2787,12 @@ function nonEmptyStrings(value) {
|
|
|
2758
2787
|
const entries = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
2759
2788
|
return entries.length > 0 ? entries : void 0;
|
|
2760
2789
|
}
|
|
2790
|
+
function formatIgnoredFieldWarning2(source, context, keys) {
|
|
2791
|
+
if (keys.length === 0) {
|
|
2792
|
+
return "";
|
|
2793
|
+
}
|
|
2794
|
+
return `${source}: ignored unknown ${context} field(s): ${keys.sort((a, b) => a.localeCompare(b)).join(", ")}`;
|
|
2795
|
+
}
|
|
2761
2796
|
function refinementDir(projectDir) {
|
|
2762
2797
|
const dir = path7.join(projectDir, "tmp", "refinements");
|
|
2763
2798
|
fs6.mkdirSync(dir, { recursive: true });
|
|
@@ -2814,7 +2849,7 @@ function printDraftSummary(root, draft, filePath) {
|
|
|
2814
2849
|
}
|
|
2815
2850
|
console.log(`[COOP] apply with: coop apply draft --from-file ${path7.relative(root, filePath)}`);
|
|
2816
2851
|
}
|
|
2817
|
-
function
|
|
2852
|
+
function parseRefinementDraftInputWithWarnings(content, source) {
|
|
2818
2853
|
const parsed = parseYamlContent2(content, source);
|
|
2819
2854
|
if (parsed.kind !== "refinement_draft" || parsed.version !== 1) {
|
|
2820
2855
|
throw new Error(`${source}: not a supported COOP refinement draft.`);
|
|
@@ -2826,7 +2861,34 @@ function parseRefinementDraftInput(content, source) {
|
|
|
2826
2861
|
const sourceId = typeof sourceRecord.id === "string" ? sourceRecord.id : "";
|
|
2827
2862
|
const sourceTitle = typeof sourceRecord.title === "string" ? sourceRecord.title : sourceId;
|
|
2828
2863
|
const proposalsRaw = Array.isArray(parsed.proposals) ? parsed.proposals : [];
|
|
2829
|
-
const
|
|
2864
|
+
const warnings = [];
|
|
2865
|
+
const draftAllowedKeys = /* @__PURE__ */ new Set(["kind", "version", "mode", "source", "summary", "generated_at", "proposals"]);
|
|
2866
|
+
const ignoredDraftKeys = Object.keys(parsed).filter((key) => !draftAllowedKeys.has(key));
|
|
2867
|
+
if (ignoredDraftKeys.length > 0) {
|
|
2868
|
+
warnings.push(formatIgnoredFieldWarning2(source, "refinement draft", ignoredDraftKeys));
|
|
2869
|
+
}
|
|
2870
|
+
const sourceAllowedKeys = /* @__PURE__ */ new Set(["id", "title"]);
|
|
2871
|
+
const ignoredSourceKeys = Object.keys(sourceRecord).filter((key) => !sourceAllowedKeys.has(key));
|
|
2872
|
+
if (ignoredSourceKeys.length > 0) {
|
|
2873
|
+
warnings.push(formatIgnoredFieldWarning2(source, "refinement draft source", ignoredSourceKeys));
|
|
2874
|
+
}
|
|
2875
|
+
const proposalAllowedKeys = /* @__PURE__ */ new Set([
|
|
2876
|
+
"action",
|
|
2877
|
+
"id",
|
|
2878
|
+
"target_id",
|
|
2879
|
+
"title",
|
|
2880
|
+
"type",
|
|
2881
|
+
"status",
|
|
2882
|
+
"track",
|
|
2883
|
+
"priority",
|
|
2884
|
+
"depends_on",
|
|
2885
|
+
"acceptance",
|
|
2886
|
+
"tests_required",
|
|
2887
|
+
"authority_refs",
|
|
2888
|
+
"derived_refs",
|
|
2889
|
+
"body"
|
|
2890
|
+
]);
|
|
2891
|
+
const proposals = proposalsRaw.map((entry, index) => {
|
|
2830
2892
|
if (!isObject2(entry)) {
|
|
2831
2893
|
throw new Error(`${source}: refinement draft proposal must be an object.`);
|
|
2832
2894
|
}
|
|
@@ -2838,6 +2900,10 @@ function parseRefinementDraftInput(content, source) {
|
|
|
2838
2900
|
if (!title) {
|
|
2839
2901
|
throw new Error(`${source}: refinement draft proposal title is required.`);
|
|
2840
2902
|
}
|
|
2903
|
+
const ignoredProposalKeys = Object.keys(entry).filter((key) => !proposalAllowedKeys.has(key));
|
|
2904
|
+
if (ignoredProposalKeys.length > 0) {
|
|
2905
|
+
warnings.push(formatIgnoredFieldWarning2(source, `refinement draft proposal ${index + 1}`, ignoredProposalKeys));
|
|
2906
|
+
}
|
|
2841
2907
|
return {
|
|
2842
2908
|
action,
|
|
2843
2909
|
id: typeof entry.id === "string" ? entry.id.trim() || void 0 : void 0,
|
|
@@ -2859,17 +2925,20 @@ function parseRefinementDraftInput(content, source) {
|
|
|
2859
2925
|
throw new Error(`${source}: refinement draft has no proposals.`);
|
|
2860
2926
|
}
|
|
2861
2927
|
return {
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2928
|
+
value: {
|
|
2929
|
+
kind: "refinement_draft",
|
|
2930
|
+
version: 1,
|
|
2931
|
+
mode: parsed.mode,
|
|
2932
|
+
source: {
|
|
2933
|
+
entity_type: parsed.mode,
|
|
2934
|
+
id: sourceId,
|
|
2935
|
+
title: sourceTitle
|
|
2936
|
+
},
|
|
2937
|
+
summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Refinement draft for ${sourceId}`,
|
|
2938
|
+
generated_at: typeof parsed.generated_at === "string" && parsed.generated_at.trim() ? parsed.generated_at.trim() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
2939
|
+
proposals
|
|
2869
2940
|
},
|
|
2870
|
-
|
|
2871
|
-
generated_at: typeof parsed.generated_at === "string" && parsed.generated_at.trim() ? parsed.generated_at.trim() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
2872
|
-
proposals
|
|
2941
|
+
warnings
|
|
2873
2942
|
};
|
|
2874
2943
|
}
|
|
2875
2944
|
function taskFromProposal(proposal, fallbackDate) {
|
|
@@ -2976,50 +3045,78 @@ function parseTaskDraftObject(record, source) {
|
|
|
2976
3045
|
const status = record.status === "todo" || record.status === "blocked" || record.status === "in_progress" || record.status === "in_review" || record.status === "done" || record.status === "canceled" ? record.status : void 0;
|
|
2977
3046
|
const priority = record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0;
|
|
2978
3047
|
const origin = isObject2(record.origin) ? record.origin : {};
|
|
3048
|
+
const taskAllowedKeys = /* @__PURE__ */ new Set([
|
|
3049
|
+
"id",
|
|
3050
|
+
"title",
|
|
3051
|
+
"type",
|
|
3052
|
+
"status",
|
|
3053
|
+
"track",
|
|
3054
|
+
"priority",
|
|
3055
|
+
"depends_on",
|
|
3056
|
+
"acceptance",
|
|
3057
|
+
"tests_required",
|
|
3058
|
+
"authority_refs",
|
|
3059
|
+
"derived_refs",
|
|
3060
|
+
"origin",
|
|
3061
|
+
"body"
|
|
3062
|
+
]);
|
|
3063
|
+
const originAllowedKeys = /* @__PURE__ */ new Set(["authority_refs", "derived_refs"]);
|
|
3064
|
+
const ignoredTaskKeys = Object.keys(record).filter((key) => !taskAllowedKeys.has(key));
|
|
3065
|
+
const ignoredOriginKeys = Object.keys(origin).filter((key) => !originAllowedKeys.has(key));
|
|
3066
|
+
const warnings = [];
|
|
3067
|
+
if (ignoredTaskKeys.length > 0) {
|
|
3068
|
+
warnings.push(formatIgnoredFieldWarning2(source, "task draft", ignoredTaskKeys));
|
|
3069
|
+
}
|
|
3070
|
+
if (ignoredOriginKeys.length > 0) {
|
|
3071
|
+
warnings.push(formatIgnoredFieldWarning2(source, "task draft origin", ignoredOriginKeys));
|
|
3072
|
+
}
|
|
2979
3073
|
return {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3074
|
+
value: {
|
|
3075
|
+
kind: "refinement_draft",
|
|
3076
|
+
version: 1,
|
|
3077
|
+
mode: "task",
|
|
3078
|
+
source: {
|
|
3079
|
+
entity_type: "task",
|
|
3080
|
+
id: id ?? "draft-task",
|
|
3081
|
+
title
|
|
3082
|
+
},
|
|
3083
|
+
summary: `Imported task draft for ${title}`,
|
|
3084
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3085
|
+
proposals: [
|
|
3086
|
+
{
|
|
3087
|
+
action: "create",
|
|
3088
|
+
id,
|
|
3089
|
+
title,
|
|
3090
|
+
type,
|
|
3091
|
+
status,
|
|
3092
|
+
track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
|
|
3093
|
+
priority,
|
|
3094
|
+
depends_on: nonEmptyStrings(record.depends_on),
|
|
3095
|
+
acceptance: nonEmptyStrings(record.acceptance),
|
|
3096
|
+
tests_required: nonEmptyStrings(record.tests_required),
|
|
3097
|
+
authority_refs: nonEmptyStrings(record.authority_refs) ?? nonEmptyStrings(origin.authority_refs),
|
|
3098
|
+
derived_refs: nonEmptyStrings(record.derived_refs) ?? nonEmptyStrings(origin.derived_refs),
|
|
3099
|
+
body: typeof record.body === "string" ? record.body : void 0
|
|
3100
|
+
}
|
|
3101
|
+
]
|
|
2987
3102
|
},
|
|
2988
|
-
|
|
2989
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2990
|
-
proposals: [
|
|
2991
|
-
{
|
|
2992
|
-
action: "create",
|
|
2993
|
-
id,
|
|
2994
|
-
title,
|
|
2995
|
-
type,
|
|
2996
|
-
status,
|
|
2997
|
-
track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
|
|
2998
|
-
priority,
|
|
2999
|
-
depends_on: nonEmptyStrings(record.depends_on),
|
|
3000
|
-
acceptance: nonEmptyStrings(record.acceptance),
|
|
3001
|
-
tests_required: nonEmptyStrings(record.tests_required),
|
|
3002
|
-
authority_refs: nonEmptyStrings(record.authority_refs) ?? nonEmptyStrings(origin.authority_refs),
|
|
3003
|
-
derived_refs: nonEmptyStrings(record.derived_refs) ?? nonEmptyStrings(origin.derived_refs),
|
|
3004
|
-
body: typeof record.body === "string" ? record.body : void 0
|
|
3005
|
-
}
|
|
3006
|
-
]
|
|
3103
|
+
warnings
|
|
3007
3104
|
};
|
|
3008
3105
|
}
|
|
3009
|
-
function
|
|
3106
|
+
function parseTaskDraftOrRefinementWithWarnings(content, source) {
|
|
3010
3107
|
const trimmed = content.trimStart();
|
|
3011
3108
|
if (trimmed.startsWith("---")) {
|
|
3012
3109
|
const { frontmatter, body } = parseFrontmatterContent2(content, source);
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
...
|
|
3016
|
-
body: body ||
|
|
3110
|
+
const parsed2 = parseTaskDraftObject(frontmatter, source);
|
|
3111
|
+
parsed2.value.proposals[0] = {
|
|
3112
|
+
...parsed2.value.proposals[0],
|
|
3113
|
+
body: body || parsed2.value.proposals[0]?.body
|
|
3017
3114
|
};
|
|
3018
|
-
return
|
|
3115
|
+
return parsed2;
|
|
3019
3116
|
}
|
|
3020
3117
|
const parsed = parseYamlContent2(content, source);
|
|
3021
3118
|
if (parsed.kind === "refinement_draft") {
|
|
3022
|
-
return
|
|
3119
|
+
return parseRefinementDraftInputWithWarnings(content, source);
|
|
3023
3120
|
}
|
|
3024
3121
|
return parseTaskDraftObject(parsed, source);
|
|
3025
3122
|
}
|
|
@@ -3035,8 +3132,12 @@ function parseCsv(value) {
|
|
|
3035
3132
|
if (!value) return [];
|
|
3036
3133
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3037
3134
|
}
|
|
3038
|
-
function
|
|
3039
|
-
|
|
3135
|
+
function collectRepeatedValue(value, previous = []) {
|
|
3136
|
+
const next = value.trim();
|
|
3137
|
+
if (!next) {
|
|
3138
|
+
return previous;
|
|
3139
|
+
}
|
|
3140
|
+
return [...previous, next];
|
|
3040
3141
|
}
|
|
3041
3142
|
function toNumber(value, field) {
|
|
3042
3143
|
if (value == null || value.trim().length === 0) return void 0;
|
|
@@ -3054,6 +3155,11 @@ function plusDaysIso(days) {
|
|
|
3054
3155
|
function unique2(values) {
|
|
3055
3156
|
return Array.from(new Set(values));
|
|
3056
3157
|
}
|
|
3158
|
+
function printDraftWarnings(warnings) {
|
|
3159
|
+
for (const warning of warnings) {
|
|
3160
|
+
console.warn(`[COOP][warn] ${warning}`);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3057
3163
|
function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
|
|
3058
3164
|
const normalizedName = candidateName.trim().toLowerCase();
|
|
3059
3165
|
if (!normalizedName) {
|
|
@@ -3089,6 +3195,30 @@ function assertKnownDynamicFields(root, fields) {
|
|
|
3089
3195
|
}
|
|
3090
3196
|
}
|
|
3091
3197
|
}
|
|
3198
|
+
async function collectRequiredNamingFields(root, entityType, fields) {
|
|
3199
|
+
const next = { ...fields };
|
|
3200
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3201
|
+
return next;
|
|
3202
|
+
}
|
|
3203
|
+
const tokens = namingTokensForRoot(root);
|
|
3204
|
+
for (const token of requiredCustomNamingTokens(root, entityType)) {
|
|
3205
|
+
const existing = next[token]?.trim();
|
|
3206
|
+
if (existing) {
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
const definition = tokens[token];
|
|
3210
|
+
if (definition && definition.values.length > 0) {
|
|
3211
|
+
const choice = await select(
|
|
3212
|
+
`Naming token ${token}`,
|
|
3213
|
+
definition.values.map((value) => ({ label: value, value }))
|
|
3214
|
+
);
|
|
3215
|
+
next[token] = choice;
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3218
|
+
next[token] = await ask(`Naming token ${token}`);
|
|
3219
|
+
}
|
|
3220
|
+
return next;
|
|
3221
|
+
}
|
|
3092
3222
|
function resolveIdeaFile2(root, idOrAlias) {
|
|
3093
3223
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
3094
3224
|
return path8.join(root, ...target.file.split("/"));
|
|
@@ -3117,11 +3247,11 @@ function makeTaskDraft(input2) {
|
|
|
3117
3247
|
}
|
|
3118
3248
|
function registerCreateCommand(program) {
|
|
3119
3249
|
const create = program.command("create").description("Create COOP entities");
|
|
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 <
|
|
3250
|
+
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 <text>", "Acceptance criterion; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--tests-required <text>", "Required test; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--authority-ref <ref>", "Authority document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).option("--derived-ref <ref>", "Derived planning document reference; repeat the flag to add multiple entries", collectRepeatedValue, []).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) => {
|
|
3121
3251
|
const root = resolveRepoRoot();
|
|
3122
3252
|
const coop = ensureCoopInitialized(root);
|
|
3123
3253
|
const interactive = Boolean(options.interactive);
|
|
3124
|
-
|
|
3254
|
+
let dynamicFields = extractDynamicTokenFlags(
|
|
3125
3255
|
["create", "task"],
|
|
3126
3256
|
[
|
|
3127
3257
|
"id",
|
|
@@ -3144,6 +3274,7 @@ function registerCreateCommand(program) {
|
|
|
3144
3274
|
]
|
|
3145
3275
|
);
|
|
3146
3276
|
assertKnownDynamicFields(root, dynamicFields);
|
|
3277
|
+
dynamicFields = await collectRequiredNamingFields(root, "task", dynamicFields);
|
|
3147
3278
|
if (options.fromFile?.trim() || options.stdin) {
|
|
3148
3279
|
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) {
|
|
3149
3280
|
throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
|
|
@@ -3152,8 +3283,10 @@ function registerCreateCommand(program) {
|
|
|
3152
3283
|
fromFile: options.fromFile,
|
|
3153
3284
|
stdin: options.stdin
|
|
3154
3285
|
});
|
|
3286
|
+
const parsedDraftResult = parseTaskDraftOrRefinementWithWarnings(draftInput.content, draftInput.source);
|
|
3287
|
+
printDraftWarnings(parsedDraftResult.warnings);
|
|
3155
3288
|
const parsedDraft = ensureValidCreateOnlyDraft(
|
|
3156
|
-
assignCreateProposalIds(root,
|
|
3289
|
+
assignCreateProposalIds(root, parsedDraftResult.value),
|
|
3157
3290
|
draftInput.source
|
|
3158
3291
|
);
|
|
3159
3292
|
const written = applyRefinementDraft(root, coop, parsedDraft);
|
|
@@ -3319,11 +3452,12 @@ function registerCreateCommand(program) {
|
|
|
3319
3452
|
const root = resolveRepoRoot();
|
|
3320
3453
|
const coop = ensureCoopInitialized(root);
|
|
3321
3454
|
const interactive = Boolean(options.interactive);
|
|
3322
|
-
|
|
3455
|
+
let dynamicFields = extractDynamicTokenFlags(
|
|
3323
3456
|
["create", "idea"],
|
|
3324
3457
|
["id", "title", "author", "source", "status", "tags", "body", "from-file", "stdin", "interactive"]
|
|
3325
3458
|
);
|
|
3326
3459
|
assertKnownDynamicFields(root, dynamicFields);
|
|
3460
|
+
dynamicFields = await collectRequiredNamingFields(root, "idea", dynamicFields);
|
|
3327
3461
|
if (options.fromFile?.trim() || options.stdin) {
|
|
3328
3462
|
if (options.id || options.title || titleArg || options.author || options.source || options.status || options.tags || options.body) {
|
|
3329
3463
|
throw new Error("Cannot combine --from-file/--stdin with direct idea field flags. Use one input mode.");
|
|
@@ -3332,7 +3466,9 @@ function registerCreateCommand(program) {
|
|
|
3332
3466
|
fromFile: options.fromFile,
|
|
3333
3467
|
stdin: options.stdin
|
|
3334
3468
|
});
|
|
3335
|
-
const
|
|
3469
|
+
const parsedIdeaDraft = parseIdeaDraftInputWithWarnings(draftInput.content, draftInput.source);
|
|
3470
|
+
printDraftWarnings(parsedIdeaDraft.warnings);
|
|
3471
|
+
const written = writeIdeaFromDraft(root, coop, parsedIdeaDraft.value);
|
|
3336
3472
|
console.log(`[COOP] created 1 idea file from ${draftInput.source}`);
|
|
3337
3473
|
console.log(`Created idea: ${path8.relative(root, written)}`);
|
|
3338
3474
|
return;
|
|
@@ -3380,11 +3516,12 @@ function registerCreateCommand(program) {
|
|
|
3380
3516
|
const root = resolveRepoRoot();
|
|
3381
3517
|
const coop = ensureCoopInitialized(root);
|
|
3382
3518
|
const interactive = Boolean(options.interactive);
|
|
3383
|
-
|
|
3519
|
+
let dynamicFields = extractDynamicTokenFlags(
|
|
3384
3520
|
["create", "track"],
|
|
3385
3521
|
["id", "name", "profiles", "max-wip", "allowed-types", "interactive"]
|
|
3386
3522
|
);
|
|
3387
3523
|
assertKnownDynamicFields(root, dynamicFields);
|
|
3524
|
+
dynamicFields = await collectRequiredNamingFields(root, "track", dynamicFields);
|
|
3388
3525
|
const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
|
|
3389
3526
|
if (!name) throw new Error("Track name is required.");
|
|
3390
3527
|
const capacityProfiles = unique2(
|
|
@@ -3436,7 +3573,7 @@ function registerCreateCommand(program) {
|
|
|
3436
3573
|
const root = resolveRepoRoot();
|
|
3437
3574
|
const coop = ensureCoopInitialized(root);
|
|
3438
3575
|
const interactive = Boolean(options.interactive);
|
|
3439
|
-
|
|
3576
|
+
let dynamicFields = extractDynamicTokenFlags(
|
|
3440
3577
|
["create", "delivery"],
|
|
3441
3578
|
[
|
|
3442
3579
|
"id",
|
|
@@ -3455,6 +3592,7 @@ function registerCreateCommand(program) {
|
|
|
3455
3592
|
]
|
|
3456
3593
|
);
|
|
3457
3594
|
assertKnownDynamicFields(root, dynamicFields);
|
|
3595
|
+
dynamicFields = await collectRequiredNamingFields(root, "delivery", dynamicFields);
|
|
3458
3596
|
const user = options.user?.trim() || defaultCoopAuthor(root);
|
|
3459
3597
|
const config = readCoopConfig(root);
|
|
3460
3598
|
const auth = load_auth_config2(config.raw);
|
|
@@ -3912,14 +4050,18 @@ var catalog = {
|
|
|
3912
4050
|
{ usage: "coop use track <id>", purpose: "Set the default working track for commands that can infer scope." },
|
|
3913
4051
|
{ usage: "coop use delivery <id>", purpose: "Set the default working delivery for commands that need delivery scope." },
|
|
3914
4052
|
{ usage: "coop use version <id>", purpose: "Set the default working version for promotion and prompt generation." },
|
|
4053
|
+
{ usage: "coop use reset", purpose: "Clear the user-local working track, delivery, and version defaults." },
|
|
3915
4054
|
{ usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
|
|
3916
4055
|
{ usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
|
|
3917
4056
|
{ usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
|
|
3918
4057
|
{ usage: "coop naming", purpose: "Explain the effective per-entity naming rules, custom tokens, and examples." },
|
|
3919
4058
|
{ 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>-<
|
|
4059
|
+
{ usage: "coop naming set task <TYPE>-<TITLE8>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand; `TITLE##` supports arbitrary 1-2 digit caps." },
|
|
4060
|
+
{ usage: "coop naming reset task", purpose: "Reset one entity's naming template back to the default." },
|
|
3921
4061
|
{ usage: "coop naming token create proj", purpose: "Create a custom naming token." },
|
|
3922
|
-
{ usage: "coop naming token
|
|
4062
|
+
{ usage: "coop naming token remove proj", purpose: "Delete a custom naming token." },
|
|
4063
|
+
{ usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." },
|
|
4064
|
+
{ usage: "coop naming token value remove proj UX", purpose: "Remove an allowed value from a naming token." }
|
|
3923
4065
|
]
|
|
3924
4066
|
},
|
|
3925
4067
|
{
|
|
@@ -3927,19 +4069,19 @@ var catalog = {
|
|
|
3927
4069
|
description: "Create ideas, tasks, tracks, and deliveries.",
|
|
3928
4070
|
commands: [
|
|
3929
4071
|
{ usage: 'coop create idea "Subscription page"', purpose: "Create an idea with a positional title." },
|
|
3930
|
-
{ usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
|
|
3931
|
-
{ usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
|
|
4072
|
+
{ usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file; COOP keeps only recognized fields, synthesizes canonical metadata, and warns on ignored fields." },
|
|
4073
|
+
{ usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin with the same schema-filtered behavior." },
|
|
3932
4074
|
{ usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
|
|
3933
4075
|
{ usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
|
|
3934
4076
|
{ usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
|
|
3935
4077
|
{ usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
|
|
3936
4078
|
{ usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
|
|
3937
4079
|
{
|
|
3938
|
-
usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,
|
|
3939
|
-
purpose: "Create a planning-grade task with acceptance
|
|
4080
|
+
usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved, client mapping documented" --acceptance "Rollback path documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
|
|
4081
|
+
purpose: "Create a planning-grade task with repeatable free-text acceptance/tests fields and origin refs."
|
|
3940
4082
|
},
|
|
3941
|
-
{ usage: "coop create task --from-file task-draft.yml", purpose: "Ingest a structured task draft file." },
|
|
3942
|
-
{ usage: "cat task.md | coop create task --stdin", purpose: "Ingest a task draft from stdin." },
|
|
4083
|
+
{ usage: "coop create task --from-file task-draft.yml", purpose: "Ingest a structured task draft file; COOP keeps only recognized fields, synthesizes canonical metadata, and warns on ignored fields." },
|
|
4084
|
+
{ usage: "cat task.md | coop create task --stdin", purpose: "Ingest a task draft from stdin with the same schema-filtered behavior." },
|
|
3943
4085
|
{ usage: "coop create delivery --name MVP --scope PM-100,PM-101", purpose: "Create a delivery from task scope." }
|
|
3944
4086
|
]
|
|
3945
4087
|
},
|
|
@@ -3951,7 +4093,7 @@ var catalog = {
|
|
|
3951
4093
|
{ usage: "coop refine idea IDEA-101 --apply", purpose: "Refine an idea and apply the resulting draft." },
|
|
3952
4094
|
{ usage: "coop refine task PM-101", purpose: "Enrich a task with planning context and execution detail." },
|
|
3953
4095
|
{ usage: "coop refine task PM-101 --input-file docs/plan.md", purpose: "Use an additional planning brief during refinement." },
|
|
3954
|
-
{ usage: "cat refinement.yml | coop apply draft --stdin", purpose: "Apply a refinement draft streamed from another process." }
|
|
4096
|
+
{ usage: "cat refinement.yml | coop apply draft --stdin", purpose: "Apply a refinement draft streamed from another process; unknown draft fields are ignored with warnings." }
|
|
3955
4097
|
]
|
|
3956
4098
|
},
|
|
3957
4099
|
{
|
|
@@ -3963,7 +4105,7 @@ var catalog = {
|
|
|
3963
4105
|
{ usage: "coop pick task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Select a specific task, optionally promote it in the current context, assign it, and move it to in_progress." },
|
|
3964
4106
|
{ usage: "coop pick task --delivery MVP --claim --actor dev1 --user lead-user", purpose: "Select the top ready task, optionally assign it, and move it to in_progress." },
|
|
3965
4107
|
{ usage: "coop start task PM-101 --promote --claim --actor dev1 --user lead-user", purpose: "Start a specific task or the top ready task if no id is provided." },
|
|
3966
|
-
{ usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context." },
|
|
4108
|
+
{ usage: "coop promote task PM-101", purpose: "Promote a task using the current working track/version context; the promoted task moves to the top of that selection lens." },
|
|
3967
4109
|
{ usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
|
|
3968
4110
|
{ usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
|
|
3969
4111
|
{ usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
|
|
@@ -3992,8 +4134,14 @@ var catalog = {
|
|
|
3992
4134
|
{ usage: "coop update PM-101 --track MVP --delivery MVP", purpose: "Update a task's home track or primary delivery without editing `.coop` files directly." },
|
|
3993
4135
|
{ usage: "coop update PM-101 --add-delivery-track MVP --priority-in MVP:p0", purpose: "Add a contributing track lens and scoped priority override." },
|
|
3994
4136
|
{ usage: "coop update PM-101 --priority p1 --add-fix-version v2", purpose: "Update task metadata without editing `.coop` files directly." },
|
|
4137
|
+
{ usage: 'coop update PM-101 --acceptance-set "Contract approved, client mapping documented" --acceptance-set "Rollback path documented"', purpose: "Replace the full acceptance list with repeatable free-text entries; useful when an older task was created with the wrong parsing." },
|
|
4138
|
+
{ usage: 'coop update PM-101 --tests-set "Contract fixture test" --tests-set "Integration smoke"', purpose: "Replace the full tests_required list with repeatable entries." },
|
|
4139
|
+
{ usage: "coop update PM-101 --authority-ref-set docs/spec.md#auth --derived-ref-set docs/plan.md#scope", purpose: "Replace the full origin reference lists without editing task files directly." },
|
|
4140
|
+
{ usage: "coop update PM-101 --deps-set PM-201 --deps-set PM-202 --delivery-tracks-set MVP", purpose: "Replace identifier-based list fields such as depends_on and delivery_tracks." },
|
|
4141
|
+
{ usage: "coop update IDEA-101 --linked-tasks-set PM-101 --linked-tasks-set PM-102", purpose: "Replace the full linked_tasks list on an idea." },
|
|
3995
4142
|
{ usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
|
|
3996
4143
|
{ usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
|
|
4144
|
+
{ usage: "coop alias remove PM-101 PAY.UPI", purpose: "Remove an alias from a task or idea." },
|
|
3997
4145
|
{ usage: "coop plan delivery MVP", purpose: "Run delivery feasibility analysis." },
|
|
3998
4146
|
{ usage: "coop plan delivery MVP --monte-carlo --iterations 5000", purpose: "Run probabilistic delivery forecasting." },
|
|
3999
4147
|
{ usage: "coop view velocity", purpose: "Show historical throughput." },
|
|
@@ -4036,7 +4184,10 @@ var catalog = {
|
|
|
4036
4184
|
execution_model: [
|
|
4037
4185
|
"Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
|
|
4038
4186
|
"Use `coop create ... --from-file|--stdin` and `coop apply draft` instead of editing `.coop` task or idea files directly.",
|
|
4187
|
+
"Draft files are input bundles, not raw frontmatter passthrough. COOP keeps only recognized fields, fills canonical metadata itself, and warns when it ignores unknown fields.",
|
|
4188
|
+
"For task creation, repeat `--acceptance`, `--tests-required`, `--authority-ref`, and `--derived-ref` to append multiple entries. Commas inside one value are preserved.",
|
|
4039
4189
|
"Use `coop update`, `coop comment`, and `coop log-time` for task mutations instead of manually editing task files.",
|
|
4190
|
+
"When a mutable list field needs correction, prefer the `--...-set` variants on `coop update` over manual file edits. This applies to acceptance, tests_required, authority_refs, derived_refs, depends_on, delivery_tracks, tags, fix_versions, released_in, and idea linked_tasks.",
|
|
4040
4191
|
"Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
|
|
4041
4192
|
"If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
|
|
4042
4193
|
],
|
|
@@ -4080,7 +4231,9 @@ function renderTopicPayload(topic) {
|
|
|
4080
4231
|
"Use `coop naming` to inspect per-entity templates, custom tokens, and examples.",
|
|
4081
4232
|
'Use `coop naming preview "<title>" --entity <entity>` before creating a new item if predictable IDs matter.',
|
|
4082
4233
|
"Use `coop naming set <entity> <template>` to update one entity's naming rule.",
|
|
4083
|
-
"
|
|
4234
|
+
"Built-in title tokens support `TITLE##` with a 1-2 digit cap, such as `TITLE18`, `TITLE12`, `TITLE8`, `TITLE08`, `TITLE4`, or `TITLE02`.",
|
|
4235
|
+
"Use `coop naming reset <entity>` to revert one entity back to the default naming template.",
|
|
4236
|
+
"Use `coop naming token create <token>`, `coop naming token remove <token>`, and `coop naming token value add|remove <token> <value>` before passing custom token flags like `--proj UX`."
|
|
4084
4237
|
]
|
|
4085
4238
|
};
|
|
4086
4239
|
}
|
|
@@ -6071,8 +6224,7 @@ function printNamingOverview() {
|
|
|
6071
6224
|
console.log("Built-in tokens:");
|
|
6072
6225
|
console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
|
|
6073
6226
|
console.log(" <TITLE> semantic title token (defaults to TITLE16)");
|
|
6074
|
-
console.log(" <
|
|
6075
|
-
console.log(" <TITLE24> semantic title token capped to 24 chars");
|
|
6227
|
+
console.log(" <TITLE##> semantic title token capped to the numeric suffix, e.g. TITLE18, TITLE8, or TITLE08");
|
|
6076
6228
|
console.log(" <TRACK> task track");
|
|
6077
6229
|
console.log(" <NAME> entity name/title");
|
|
6078
6230
|
console.log(" <NAME_SLUG> lower-case slug of the entity name");
|
|
@@ -6107,7 +6259,7 @@ function printNamingOverview() {
|
|
|
6107
6259
|
}, root)}`);
|
|
6108
6260
|
console.log("Try:");
|
|
6109
6261
|
console.log(` coop naming preview "${sampleTitle}"`);
|
|
6110
|
-
console.log(" coop naming set task <TYPE>-<
|
|
6262
|
+
console.log(" coop naming set task <TYPE>-<TITLE8>-<SEQ>");
|
|
6111
6263
|
console.log(" coop naming token create proj");
|
|
6112
6264
|
console.log(" coop naming token value add proj UX");
|
|
6113
6265
|
}
|
|
@@ -6126,6 +6278,17 @@ function setEntityNamingTemplate(root, entity, template) {
|
|
|
6126
6278
|
return next;
|
|
6127
6279
|
});
|
|
6128
6280
|
}
|
|
6281
|
+
function resetEntityNamingTemplate(root, entity) {
|
|
6282
|
+
writeNamingConfig(root, (config) => {
|
|
6283
|
+
const next = { ...config };
|
|
6284
|
+
const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
|
|
6285
|
+
const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
|
|
6286
|
+
namingRaw[entity] = DEFAULT_NAMING_TEMPLATES[entity];
|
|
6287
|
+
idRaw.naming = namingRaw;
|
|
6288
|
+
next.id = idRaw;
|
|
6289
|
+
return next;
|
|
6290
|
+
});
|
|
6291
|
+
}
|
|
6129
6292
|
function ensureTokenRecord(config) {
|
|
6130
6293
|
const next = { ...config };
|
|
6131
6294
|
const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
|
|
@@ -6187,6 +6350,12 @@ function registerNamingCommand(program) {
|
|
|
6187
6350
|
setEntityNamingTemplate(root, assertNamingEntity(entity), template);
|
|
6188
6351
|
console.log(`${assertNamingEntity(entity)}=${namingTemplatesForRoot(root)[assertNamingEntity(entity)]}`);
|
|
6189
6352
|
});
|
|
6353
|
+
naming.command("reset").description("Reset the naming template for one entity type to the default").argument("<entity>", "Entity: task|idea|track|delivery|run").action((entity) => {
|
|
6354
|
+
const root = resolveRepoRoot();
|
|
6355
|
+
const resolved = assertNamingEntity(entity);
|
|
6356
|
+
resetEntityNamingTemplate(root, resolved);
|
|
6357
|
+
console.log(`${resolved}=${namingTemplatesForRoot(root)[resolved]}`);
|
|
6358
|
+
});
|
|
6190
6359
|
const token = naming.command("token").description("Manage custom naming tokens");
|
|
6191
6360
|
token.command("list").description("List naming tokens").action(() => {
|
|
6192
6361
|
listTokens();
|
|
@@ -6223,7 +6392,7 @@ function registerNamingCommand(program) {
|
|
|
6223
6392
|
});
|
|
6224
6393
|
console.log(`Renamed naming token: ${normalizedToken} -> ${normalizedNext}`);
|
|
6225
6394
|
});
|
|
6226
|
-
token.command("delete").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
|
|
6395
|
+
token.command("delete").alias("remove").alias("rm").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
|
|
6227
6396
|
const root = resolveRepoRoot();
|
|
6228
6397
|
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6229
6398
|
writeNamingConfig(root, (config) => {
|
|
@@ -6960,7 +7129,11 @@ function registerRefineCommand(program) {
|
|
|
6960
7129
|
const root = resolveRepoRoot();
|
|
6961
7130
|
const projectDir = ensureCoopInitialized(root);
|
|
6962
7131
|
const draftInput = await readDraftContent(root, options);
|
|
6963
|
-
const
|
|
7132
|
+
const parsedDraft = parseRefinementDraftInputWithWarnings(draftInput.content, draftInput.source);
|
|
7133
|
+
for (const warning of parsedDraft.warnings) {
|
|
7134
|
+
console.warn(`[COOP][warn] ${warning}`);
|
|
7135
|
+
}
|
|
7136
|
+
const draft = parsedDraft.value;
|
|
6964
7137
|
const written = applyRefinementDraft(root, projectDir, draft);
|
|
6965
7138
|
console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
|
|
6966
7139
|
for (const filePath of written) {
|
|
@@ -8070,6 +8243,13 @@ function addValues(source, values) {
|
|
|
8070
8243
|
const next = unique3([...source ?? [], ...values ?? []]);
|
|
8071
8244
|
return next.length > 0 ? next : void 0;
|
|
8072
8245
|
}
|
|
8246
|
+
function setValues(values) {
|
|
8247
|
+
if (!values || values.length === 0) {
|
|
8248
|
+
return void 0;
|
|
8249
|
+
}
|
|
8250
|
+
const next = unique3(values);
|
|
8251
|
+
return next.length > 0 ? next : void 0;
|
|
8252
|
+
}
|
|
8073
8253
|
function loadBody(options) {
|
|
8074
8254
|
if (options.bodyFile) {
|
|
8075
8255
|
return fs20.readFileSync(options.bodyFile, "utf8");
|
|
@@ -8123,6 +8303,29 @@ function normalizeTaskPriority(priority) {
|
|
|
8123
8303
|
function renderTaskPreview(task, body) {
|
|
8124
8304
|
return stringifyFrontmatter5(task, body);
|
|
8125
8305
|
}
|
|
8306
|
+
function normalizeOrigin(task, authorityRefs, derivedRefs) {
|
|
8307
|
+
const nextOrigin = {
|
|
8308
|
+
...task.origin ?? {},
|
|
8309
|
+
authority_refs: authorityRefs,
|
|
8310
|
+
derived_refs: derivedRefs
|
|
8311
|
+
};
|
|
8312
|
+
const hasAuthority = Array.isArray(nextOrigin.authority_refs) && nextOrigin.authority_refs.length > 0;
|
|
8313
|
+
const hasDerived = Array.isArray(nextOrigin.derived_refs) && nextOrigin.derived_refs.length > 0;
|
|
8314
|
+
const hasPromoted = Array.isArray(nextOrigin.promoted_from) && nextOrigin.promoted_from.length > 0;
|
|
8315
|
+
if (!hasAuthority) {
|
|
8316
|
+
delete nextOrigin.authority_refs;
|
|
8317
|
+
}
|
|
8318
|
+
if (!hasDerived) {
|
|
8319
|
+
delete nextOrigin.derived_refs;
|
|
8320
|
+
}
|
|
8321
|
+
if (!hasPromoted) {
|
|
8322
|
+
delete nextOrigin.promoted_from;
|
|
8323
|
+
}
|
|
8324
|
+
return {
|
|
8325
|
+
...task,
|
|
8326
|
+
origin: hasAuthority || hasDerived || hasPromoted ? nextOrigin : void 0
|
|
8327
|
+
};
|
|
8328
|
+
}
|
|
8126
8329
|
function updateTask(id, options) {
|
|
8127
8330
|
const root = resolveRepoRoot();
|
|
8128
8331
|
const { filePath, parsed } = loadTaskEntry(root, id);
|
|
@@ -8138,16 +8341,19 @@ function updateTask(id, options) {
|
|
|
8138
8341
|
if (options.delivery !== void 0) next.delivery = options.delivery.trim() || null;
|
|
8139
8342
|
if (options.storyPoints !== void 0) next.story_points = Number(options.storyPoints);
|
|
8140
8343
|
if (options.plannedHours !== void 0) next = setTaskPlannedHours(next, Number(options.plannedHours));
|
|
8344
|
+
const nextAuthorityRefs = options.authorityRefSet && options.authorityRefSet.length > 0 ? setValues(options.authorityRefSet) : addValues(removeValues(next.origin?.authority_refs, options.authorityRefRemove), options.authorityRefAdd);
|
|
8345
|
+
const nextDerivedRefs = options.derivedRefSet && options.derivedRefSet.length > 0 ? setValues(options.derivedRefSet) : addValues(removeValues(next.origin?.derived_refs, options.derivedRefRemove), options.derivedRefAdd);
|
|
8141
8346
|
next = {
|
|
8142
8347
|
...next,
|
|
8143
|
-
delivery_tracks: addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
|
|
8144
|
-
depends_on: addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
|
|
8145
|
-
tags: addValues(removeValues(next.tags, options.removeTag), options.addTag),
|
|
8146
|
-
fix_versions: addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
|
|
8147
|
-
released_in: addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
|
|
8148
|
-
acceptance: addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
|
|
8149
|
-
tests_required: addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
|
|
8348
|
+
delivery_tracks: options.deliveryTracksSet && options.deliveryTracksSet.length > 0 ? setValues(options.deliveryTracksSet) : addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
|
|
8349
|
+
depends_on: options.depsSet && options.depsSet.length > 0 ? setValues(options.depsSet) : addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
|
|
8350
|
+
tags: options.tagsSet && options.tagsSet.length > 0 ? setValues(options.tagsSet) : addValues(removeValues(next.tags, options.removeTag), options.addTag),
|
|
8351
|
+
fix_versions: options.fixVersionsSet && options.fixVersionsSet.length > 0 ? setValues(options.fixVersionsSet) : addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
|
|
8352
|
+
released_in: options.releasedInSet && options.releasedInSet.length > 0 ? setValues(options.releasedInSet) : addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
|
|
8353
|
+
acceptance: options.acceptanceSet && options.acceptanceSet.length > 0 ? setValues(options.acceptanceSet) : addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
|
|
8354
|
+
tests_required: options.testsSet && options.testsSet.length > 0 ? setValues(options.testsSet) : addValues(removeValues(next.tests_required, options.testsRemove), options.testsAdd)
|
|
8150
8355
|
};
|
|
8356
|
+
next = normalizeOrigin(next, nextAuthorityRefs, nextDerivedRefs);
|
|
8151
8357
|
next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
|
|
8152
8358
|
next = validateTaskForWrite(root, next, filePath);
|
|
8153
8359
|
const nextBody = loadBody(options) ?? parsed.body;
|
|
@@ -8169,8 +8375,8 @@ function updateIdea(id, options) {
|
|
|
8169
8375
|
...parsed.idea,
|
|
8170
8376
|
title: options.title?.trim() || parsed.idea.title,
|
|
8171
8377
|
status: nextStatus || parsed.idea.status,
|
|
8172
|
-
tags: addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag) ?? [],
|
|
8173
|
-
linked_tasks: addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask) ?? []
|
|
8378
|
+
tags: (options.tagsSet && options.tagsSet.length > 0 ? setValues(options.tagsSet) : addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag)) ?? [],
|
|
8379
|
+
linked_tasks: (options.linkedTasksSet && options.linkedTasksSet.length > 0 ? setValues(options.linkedTasksSet) : addValues(removeValues(parsed.idea.linked_tasks, options.removeLinkedTask), options.addLinkedTask)) ?? []
|
|
8174
8380
|
};
|
|
8175
8381
|
const nextBody = loadBody(options) ?? parsed.body;
|
|
8176
8382
|
if (options.dryRun) {
|
|
@@ -8181,7 +8387,7 @@ function updateIdea(id, options) {
|
|
|
8181
8387
|
console.log(`Updated ${next.id}`);
|
|
8182
8388
|
}
|
|
8183
8389
|
function registerUpdateCommand(program) {
|
|
8184
|
-
program.command("update").description("Update an existing COOP task or idea").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("[id]", "Entity id when an explicit type is provided").option("--title <title>").option("--priority <priority>").option("--status <status>").option("--assign <user>").option("--track <id>", "Set the task home/origin track").option("--delivery <id>", "Set the task primary delivery id").option("--story-points <n>").option("--planned-hours <n>").option("--add-delivery-track <id>", "", collect, []).option("--remove-delivery-track <id>", "", collect, []).option("--priority-in <track:priority>", "", collect, []).option("--clear-priority-in <track>", "", collect, []).option("--add-dep <id>", "", collect, []).option("--remove-dep <id>", "", collect, []).option("--add-tag <tag>", "", collect, []).option("--remove-tag <tag>", "", collect, []).option("--add-fix-version <v>", "", collect, []).option("--remove-fix-version <v>", "", collect, []).option("--add-released-in <v>", "", collect, []).option("--remove-released-in <v>", "", collect, []).option("--acceptance-add <text>", "", collect, []).option("--acceptance-remove <text>", "", collect, []).option("--tests-add <text>", "", collect, []).option("--tests-remove <text>", "", collect, []).option("--add-linked-task <id>", "", collect, []).option("--remove-linked-task <id>", "", collect, []).option("--body-file <path>").option("--body-stdin").option("--dry-run").action((first, second, options) => {
|
|
8390
|
+
program.command("update").description("Update an existing COOP task or idea").argument("<id-or-type>", "Task or idea id/alias, or an explicit entity type").argument("[id]", "Entity id when an explicit type is provided").option("--title <title>").option("--priority <priority>").option("--status <status>").option("--assign <user>").option("--track <id>", "Set the task home/origin track").option("--delivery <id>", "Set the task primary delivery id").option("--story-points <n>").option("--planned-hours <n>").option("--delivery-tracks-set <id>", "Replace the full delivery_tracks list", collect, []).option("--add-delivery-track <id>", "", collect, []).option("--remove-delivery-track <id>", "", collect, []).option("--priority-in <track:priority>", "", collect, []).option("--clear-priority-in <track>", "", collect, []).option("--deps-set <id>", "Replace the full depends_on list", collect, []).option("--add-dep <id>", "", collect, []).option("--remove-dep <id>", "", collect, []).option("--tags-set <tag>", "Replace the full tags list", collect, []).option("--add-tag <tag>", "", collect, []).option("--remove-tag <tag>", "", collect, []).option("--fix-versions-set <v>", "Replace the full fix_versions list", collect, []).option("--add-fix-version <v>", "", collect, []).option("--remove-fix-version <v>", "", collect, []).option("--released-in-set <v>", "Replace the full released_in list", collect, []).option("--add-released-in <v>", "", collect, []).option("--remove-released-in <v>", "", collect, []).option("--authority-ref-set <ref>", "Replace the full authority_refs list", collect, []).option("--authority-ref-add <ref>", "", collect, []).option("--authority-ref-remove <ref>", "", collect, []).option("--derived-ref-set <ref>", "Replace the full derived_refs list", collect, []).option("--derived-ref-add <ref>", "", collect, []).option("--derived-ref-remove <ref>", "", collect, []).option("--acceptance-set <text>", "Replace the full acceptance list; repeat the flag to set multiple entries", collect, []).option("--acceptance-add <text>", "", collect, []).option("--acceptance-remove <text>", "", collect, []).option("--tests-set <text>", "Replace the full tests_required list; repeat the flag to set multiple entries", collect, []).option("--tests-add <text>", "", collect, []).option("--tests-remove <text>", "", collect, []).option("--linked-tasks-set <id>", "Replace the full linked_tasks list on an idea", collect, []).option("--add-linked-task <id>", "", collect, []).option("--remove-linked-task <id>", "", collect, []).option("--body-file <path>").option("--body-stdin").option("--dry-run").action((first, second, options) => {
|
|
8185
8391
|
let resolved = resolveOptionalEntityArg(first, second, ["task", "idea"], "task");
|
|
8186
8392
|
if (!second && resolved.entity === "task") {
|
|
8187
8393
|
try {
|
|
@@ -8731,16 +8937,22 @@ function renderBasicHelp() {
|
|
|
8731
8937
|
"",
|
|
8732
8938
|
"Day-to-day commands:",
|
|
8733
8939
|
"- `coop current`: show working context, active work, and the next ready task",
|
|
8734
|
-
"- `coop use track <id>` / `coop use delivery <id
|
|
8940
|
+
"- `coop use track <id>` / `coop use delivery <id>` / `coop use reset`: manage working scope defaults",
|
|
8735
8941
|
"- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
|
|
8736
8942
|
"- `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',
|
|
8943
|
+
'- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item; templates support `TITLE##` like `TITLE18`, `TITLE8`, or `TITLE08`',
|
|
8944
|
+
"- `coop naming reset task`: reset one entity's naming template to the default",
|
|
8945
|
+
"- `coop create task --from-file draft.yml` / `coop create idea --stdin`: ingest structured drafts; COOP keeps only recognized fields, fills metadata, and warns on ignored fields",
|
|
8738
8946
|
"- `coop next task` or `coop pick task`: choose work from COOP",
|
|
8947
|
+
"- `coop promote <id>`: move a task to the top of the current working track/version selection lens",
|
|
8739
8948
|
"- `coop show <id>`: inspect a task, idea, or delivery",
|
|
8740
8949
|
"- `coop list tasks --track <id>`: browse scoped work",
|
|
8741
8950
|
"- `coop update <id> --track <id> --delivery <id>`: update task metadata",
|
|
8951
|
+
'- `coop update <id> --acceptance-set "..." --acceptance-set "..."`: replace the whole acceptance list cleanly when an old task was created with the wrong parsing',
|
|
8952
|
+
"- `coop update <id> --tests-set ... --authority-ref-set ... --deps-set ...`: the same full-replace pattern works for other mutable list fields too",
|
|
8742
8953
|
'- `coop comment <id> --message "..."`: append a task comment',
|
|
8743
8954
|
"- `coop log-time <id> --hours 2 --kind worked`: append time spent",
|
|
8955
|
+
"- `coop alias remove <id> <alias>`: remove a shorthand alias from a task or idea",
|
|
8744
8956
|
"- `coop review task <id>` / `coop complete task <id>`: move work through lifecycle",
|
|
8745
8957
|
"- `coop help-ai --initial-prompt --strict --repo C:/path/to/repo --delivery MVP --command coop.cmd`: hand off COOP context to an agent",
|
|
8746
8958
|
"",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitsy/coop",
|
|
3
3
|
"description": "COOP command-line interface.",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.5",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"commander": "^14.0.0",
|
|
41
41
|
"octokit": "^5.0.5",
|
|
42
|
-
"@kitsy/coop-ai": "2.2.
|
|
43
|
-
"@kitsy/coop-
|
|
44
|
-
"@kitsy/coop-
|
|
42
|
+
"@kitsy/coop-ai": "2.2.5",
|
|
43
|
+
"@kitsy/coop-core": "2.2.5",
|
|
44
|
+
"@kitsy/coop-ui": "^2.2.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^24.12.0",
|