@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.
Files changed (2) hide show
  1. package/dist/index.js +314 -102
  2. 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 - (segments.length > 0 ? 1 : 0);
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 + (segments.length > 1 ? 1 : 0);
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
- return contextMap[upper] || contextMap.TITLE;
511
+ const parsed = Number(upper.slice("TITLE".length));
512
+ if (Number.isInteger(parsed) && parsed > 0) {
513
+ return semanticTitleToken(contextMap.TITLE_SOURCE ?? "", parsed);
514
+ }
515
+ return contextMap.TITLE;
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 (!BUILT_IN_NAMING_TOKENS.has(token) && !config.idTokens[token.toLowerCase()]) {
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 (BUILT_IN_NAMING_TOKENS.has(token)) {
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
- id: typeof record.id === "string" && record.id.trim() ? record.id.trim().toUpperCase() : void 0,
2674
- title,
2675
- author: typeof record.author === "string" && record.author.trim() ? record.author.trim() : void 0,
2676
- source: typeof record.source === "string" && record.source.trim() ? record.source.trim() : void 0,
2677
- status: status ? status : void 0,
2678
- tags: asUniqueStrings(record.tags),
2679
- aliases: asUniqueStrings(record.aliases),
2680
- linked_tasks: asUniqueStrings(record.linked_tasks),
2681
- body: typeof record.body === "string" ? record.body : void 0
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 parseIdeaDraftInput(content, source) {
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
- ...parseIdeaDraftObject(frontmatter, source),
2690
- body: body || (typeof frontmatter.body === "string" ? frontmatter.body : void 0)
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 parseRefinementDraftInput(content, source) {
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 proposals = proposalsRaw.map((entry) => {
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
- kind: "refinement_draft",
2863
- version: 1,
2864
- mode: parsed.mode,
2865
- source: {
2866
- entity_type: parsed.mode,
2867
- id: sourceId,
2868
- title: sourceTitle
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
- summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Refinement draft for ${sourceId}`,
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
- kind: "refinement_draft",
2981
- version: 1,
2982
- mode: "task",
2983
- source: {
2984
- entity_type: "task",
2985
- id: id ?? "draft-task",
2986
- title
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
- summary: `Imported task draft for ${title}`,
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 parseTaskDraftOrRefinement(content, source) {
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 draft = parseTaskDraftObject(frontmatter, source);
3014
- draft.proposals[0] = {
3015
- ...draft.proposals[0],
3016
- body: body || draft.proposals[0]?.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 draft;
3115
+ return parsed2;
3019
3116
  }
3020
3117
  const parsed = parseYamlContent2(content, source);
3021
3118
  if (parsed.kind === "refinement_draft") {
3022
- return parseRefinementDraftInput(content, source);
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 collectMultiValue(value, previous = []) {
3039
- return [...previous, ...parseCsv(value)];
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 <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) => {
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
- const dynamicFields = extractDynamicTokenFlags(
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, parseTaskDraftOrRefinement(draftInput.content, draftInput.source)),
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
- const dynamicFields = extractDynamicTokenFlags(
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 written = writeIdeaFromDraft(root, coop, parseIdeaDraftInput(draftInput.content, draftInput.source));
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
- const dynamicFields = extractDynamicTokenFlags(
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
- const dynamicFields = extractDynamicTokenFlags(
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>-<TITLE16>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand." },
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 value add proj UX", purpose: "Register an allowed value for a 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,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
3939
- purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
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
- "Use `coop naming token create <token>` and `coop naming token value add <token> <value>` before passing custom token flags like `--proj UX`."
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(" <TITLE16> semantic title token capped to 16 chars");
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>-<TITLE24>-<SEQ>");
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 draft = parseRefinementDraftInput(draftInput.content, draftInput.source);
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>`: set working scope defaults",
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.3",
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.3",
43
- "@kitsy/coop-ui": "^2.2.3",
44
- "@kitsy/coop-core": "2.2.3"
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",