@kitsy/coop 2.2.4 → 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 +228 -81
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1888,6 +1888,8 @@ function promoteTaskForContext(task, context) {
1888
1888
  const next = {
1889
1889
  ...task,
1890
1890
  updated: todayIsoDate(),
1891
+ promoted_at: (/* @__PURE__ */ new Date()).toISOString(),
1892
+ promoted_track: context.track?.trim() || null,
1891
1893
  fix_versions: [...task.fix_versions ?? []],
1892
1894
  delivery_tracks: [...task.delivery_tracks ?? []],
1893
1895
  priority_context: { ...task.priority_context ?? {} }
@@ -2672,6 +2674,12 @@ function asUniqueStrings(value) {
2672
2674
  );
2673
2675
  return entries.length > 0 ? entries : void 0;
2674
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
+ }
2675
2683
  function parseIdeaDraftObject(record, source) {
2676
2684
  const title = typeof record.title === "string" ? record.title.trim() : "";
2677
2685
  if (!title) {
@@ -2681,25 +2689,34 @@ function parseIdeaDraftObject(record, source) {
2681
2689
  if (status && !Object.values(IdeaStatus).includes(status)) {
2682
2690
  throw new Error(`${source}: invalid idea status '${status}'.`);
2683
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));
2684
2694
  return {
2685
- id: typeof record.id === "string" && record.id.trim() ? record.id.trim().toUpperCase() : void 0,
2686
- title,
2687
- author: typeof record.author === "string" && record.author.trim() ? record.author.trim() : void 0,
2688
- source: typeof record.source === "string" && record.source.trim() ? record.source.trim() : void 0,
2689
- status: status ? status : void 0,
2690
- tags: asUniqueStrings(record.tags),
2691
- aliases: asUniqueStrings(record.aliases),
2692
- linked_tasks: asUniqueStrings(record.linked_tasks),
2693
- 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)] : []
2694
2707
  };
2695
2708
  }
2696
- function parseIdeaDraftInput(content, source) {
2709
+ function parseIdeaDraftInputWithWarnings(content, source) {
2697
2710
  const trimmed = content.trimStart();
2698
2711
  if (trimmed.startsWith("---")) {
2699
2712
  const { frontmatter, body } = parseFrontmatterContent(content, source);
2713
+ const parsed = parseIdeaDraftObject(frontmatter, source);
2700
2714
  return {
2701
- ...parseIdeaDraftObject(frontmatter, source),
2702
- 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
2703
2720
  };
2704
2721
  }
2705
2722
  return parseIdeaDraftObject(parseYamlContent(content, source), source);
@@ -2770,6 +2787,12 @@ function nonEmptyStrings(value) {
2770
2787
  const entries = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2771
2788
  return entries.length > 0 ? entries : void 0;
2772
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
+ }
2773
2796
  function refinementDir(projectDir) {
2774
2797
  const dir = path7.join(projectDir, "tmp", "refinements");
2775
2798
  fs6.mkdirSync(dir, { recursive: true });
@@ -2826,7 +2849,7 @@ function printDraftSummary(root, draft, filePath) {
2826
2849
  }
2827
2850
  console.log(`[COOP] apply with: coop apply draft --from-file ${path7.relative(root, filePath)}`);
2828
2851
  }
2829
- function parseRefinementDraftInput(content, source) {
2852
+ function parseRefinementDraftInputWithWarnings(content, source) {
2830
2853
  const parsed = parseYamlContent2(content, source);
2831
2854
  if (parsed.kind !== "refinement_draft" || parsed.version !== 1) {
2832
2855
  throw new Error(`${source}: not a supported COOP refinement draft.`);
@@ -2838,7 +2861,34 @@ function parseRefinementDraftInput(content, source) {
2838
2861
  const sourceId = typeof sourceRecord.id === "string" ? sourceRecord.id : "";
2839
2862
  const sourceTitle = typeof sourceRecord.title === "string" ? sourceRecord.title : sourceId;
2840
2863
  const proposalsRaw = Array.isArray(parsed.proposals) ? parsed.proposals : [];
2841
- 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) => {
2842
2892
  if (!isObject2(entry)) {
2843
2893
  throw new Error(`${source}: refinement draft proposal must be an object.`);
2844
2894
  }
@@ -2850,6 +2900,10 @@ function parseRefinementDraftInput(content, source) {
2850
2900
  if (!title) {
2851
2901
  throw new Error(`${source}: refinement draft proposal title is required.`);
2852
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
+ }
2853
2907
  return {
2854
2908
  action,
2855
2909
  id: typeof entry.id === "string" ? entry.id.trim() || void 0 : void 0,
@@ -2871,17 +2925,20 @@ function parseRefinementDraftInput(content, source) {
2871
2925
  throw new Error(`${source}: refinement draft has no proposals.`);
2872
2926
  }
2873
2927
  return {
2874
- kind: "refinement_draft",
2875
- version: 1,
2876
- mode: parsed.mode,
2877
- source: {
2878
- entity_type: parsed.mode,
2879
- id: sourceId,
2880
- 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
2881
2940
  },
2882
- summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Refinement draft for ${sourceId}`,
2883
- generated_at: typeof parsed.generated_at === "string" && parsed.generated_at.trim() ? parsed.generated_at.trim() : (/* @__PURE__ */ new Date()).toISOString(),
2884
- proposals
2941
+ warnings
2885
2942
  };
2886
2943
  }
2887
2944
  function taskFromProposal(proposal, fallbackDate) {
@@ -2988,50 +3045,78 @@ function parseTaskDraftObject(record, source) {
2988
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;
2989
3046
  const priority = record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0;
2990
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
+ }
2991
3073
  return {
2992
- kind: "refinement_draft",
2993
- version: 1,
2994
- mode: "task",
2995
- source: {
2996
- entity_type: "task",
2997
- id: id ?? "draft-task",
2998
- 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
+ ]
2999
3102
  },
3000
- summary: `Imported task draft for ${title}`,
3001
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3002
- proposals: [
3003
- {
3004
- action: "create",
3005
- id,
3006
- title,
3007
- type,
3008
- status,
3009
- track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
3010
- priority,
3011
- depends_on: nonEmptyStrings(record.depends_on),
3012
- acceptance: nonEmptyStrings(record.acceptance),
3013
- tests_required: nonEmptyStrings(record.tests_required),
3014
- authority_refs: nonEmptyStrings(record.authority_refs) ?? nonEmptyStrings(origin.authority_refs),
3015
- derived_refs: nonEmptyStrings(record.derived_refs) ?? nonEmptyStrings(origin.derived_refs),
3016
- body: typeof record.body === "string" ? record.body : void 0
3017
- }
3018
- ]
3103
+ warnings
3019
3104
  };
3020
3105
  }
3021
- function parseTaskDraftOrRefinement(content, source) {
3106
+ function parseTaskDraftOrRefinementWithWarnings(content, source) {
3022
3107
  const trimmed = content.trimStart();
3023
3108
  if (trimmed.startsWith("---")) {
3024
3109
  const { frontmatter, body } = parseFrontmatterContent2(content, source);
3025
- const draft = parseTaskDraftObject(frontmatter, source);
3026
- draft.proposals[0] = {
3027
- ...draft.proposals[0],
3028
- 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
3029
3114
  };
3030
- return draft;
3115
+ return parsed2;
3031
3116
  }
3032
3117
  const parsed = parseYamlContent2(content, source);
3033
3118
  if (parsed.kind === "refinement_draft") {
3034
- return parseRefinementDraftInput(content, source);
3119
+ return parseRefinementDraftInputWithWarnings(content, source);
3035
3120
  }
3036
3121
  return parseTaskDraftObject(parsed, source);
3037
3122
  }
@@ -3047,8 +3132,12 @@ function parseCsv(value) {
3047
3132
  if (!value) return [];
3048
3133
  return value.split(",").map((entry) => entry.trim()).filter(Boolean);
3049
3134
  }
3050
- function collectMultiValue(value, previous = []) {
3051
- 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];
3052
3141
  }
3053
3142
  function toNumber(value, field) {
3054
3143
  if (value == null || value.trim().length === 0) return void 0;
@@ -3066,6 +3155,11 @@ function plusDaysIso(days) {
3066
3155
  function unique2(values) {
3067
3156
  return Array.from(new Set(values));
3068
3157
  }
3158
+ function printDraftWarnings(warnings) {
3159
+ for (const warning of warnings) {
3160
+ console.warn(`[COOP][warn] ${warning}`);
3161
+ }
3162
+ }
3069
3163
  function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
3070
3164
  const normalizedName = candidateName.trim().toLowerCase();
3071
3165
  if (!normalizedName) {
@@ -3153,7 +3247,7 @@ function makeTaskDraft(input2) {
3153
3247
  }
3154
3248
  function registerCreateCommand(program) {
3155
3249
  const create = program.command("create").description("Create COOP entities");
3156
- create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
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) => {
3157
3251
  const root = resolveRepoRoot();
3158
3252
  const coop = ensureCoopInitialized(root);
3159
3253
  const interactive = Boolean(options.interactive);
@@ -3189,8 +3283,10 @@ function registerCreateCommand(program) {
3189
3283
  fromFile: options.fromFile,
3190
3284
  stdin: options.stdin
3191
3285
  });
3286
+ const parsedDraftResult = parseTaskDraftOrRefinementWithWarnings(draftInput.content, draftInput.source);
3287
+ printDraftWarnings(parsedDraftResult.warnings);
3192
3288
  const parsedDraft = ensureValidCreateOnlyDraft(
3193
- assignCreateProposalIds(root, parseTaskDraftOrRefinement(draftInput.content, draftInput.source)),
3289
+ assignCreateProposalIds(root, parsedDraftResult.value),
3194
3290
  draftInput.source
3195
3291
  );
3196
3292
  const written = applyRefinementDraft(root, coop, parsedDraft);
@@ -3370,7 +3466,9 @@ function registerCreateCommand(program) {
3370
3466
  fromFile: options.fromFile,
3371
3467
  stdin: options.stdin
3372
3468
  });
3373
- 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);
3374
3472
  console.log(`[COOP] created 1 idea file from ${draftInput.source}`);
3375
3473
  console.log(`Created idea: ${path8.relative(root, written)}`);
3376
3474
  return;
@@ -3971,19 +4069,19 @@ var catalog = {
3971
4069
  description: "Create ideas, tasks, tracks, and deliveries.",
3972
4070
  commands: [
3973
4071
  { usage: 'coop create idea "Subscription page"', purpose: "Create an idea with a positional title." },
3974
- { usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
3975
- { 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." },
3976
4074
  { usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
3977
4075
  { usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
3978
4076
  { usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
3979
4077
  { usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
3980
4078
  { usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
3981
4079
  {
3982
- usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
3983
- 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."
3984
4082
  },
3985
- { usage: "coop create task --from-file task-draft.yml", purpose: "Ingest a structured task draft file." },
3986
- { 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." },
3987
4085
  { usage: "coop create delivery --name MVP --scope PM-100,PM-101", purpose: "Create a delivery from task scope." }
3988
4086
  ]
3989
4087
  },
@@ -3995,7 +4093,7 @@ var catalog = {
3995
4093
  { usage: "coop refine idea IDEA-101 --apply", purpose: "Refine an idea and apply the resulting draft." },
3996
4094
  { usage: "coop refine task PM-101", purpose: "Enrich a task with planning context and execution detail." },
3997
4095
  { usage: "coop refine task PM-101 --input-file docs/plan.md", purpose: "Use an additional planning brief during refinement." },
3998
- { 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." }
3999
4097
  ]
4000
4098
  },
4001
4099
  {
@@ -4007,7 +4105,7 @@ var catalog = {
4007
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." },
4008
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." },
4009
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." },
4010
- { 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." },
4011
4109
  { usage: "coop review task PM-101", purpose: "Move an in-progress task into in_review using a DX-friendly verb." },
4012
4110
  { usage: "coop complete task PM-101", purpose: "Move a task in review into done using a DX-friendly verb." },
4013
4111
  { usage: "coop block task PM-101", purpose: "Mark a task as blocked." },
@@ -4036,6 +4134,11 @@ var catalog = {
4036
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." },
4037
4135
  { usage: "coop update PM-101 --add-delivery-track MVP --priority-in MVP:p0", purpose: "Add a contributing track lens and scoped priority override." },
4038
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." },
4039
4142
  { usage: 'coop comment PM-101 --message "Needs API review"', purpose: "Append a comment to a task." },
4040
4143
  { usage: 'coop log-time PM-101 --hours 2 --kind worked --note "pairing"', purpose: "Append a planned or worked time log to a task." },
4041
4144
  { usage: "coop alias remove PM-101 PAY.UPI", purpose: "Remove an alias from a task or idea." },
@@ -4081,7 +4184,10 @@ var catalog = {
4081
4184
  execution_model: [
4082
4185
  "Agents or services may send drafts through files or stdin, but COOP owns canonical writes.",
4083
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.",
4084
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.",
4085
4191
  "Use `coop log --last --verbose` when command execution fails and the concise error points to a stack trace.",
4086
4192
  "If a workflow depends on stable human-readable IDs, inspect `coop naming` or `coop config id.naming` before creating items."
4087
4193
  ],
@@ -7023,7 +7129,11 @@ function registerRefineCommand(program) {
7023
7129
  const root = resolveRepoRoot();
7024
7130
  const projectDir = ensureCoopInitialized(root);
7025
7131
  const draftInput = await readDraftContent(root, options);
7026
- 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;
7027
7137
  const written = applyRefinementDraft(root, projectDir, draft);
7028
7138
  console.log(`[COOP] applied draft from ${draftInput.source}: ${written.length} task file(s) updated`);
7029
7139
  for (const filePath of written) {
@@ -8133,6 +8243,13 @@ function addValues(source, values) {
8133
8243
  const next = unique3([...source ?? [], ...values ?? []]);
8134
8244
  return next.length > 0 ? next : void 0;
8135
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
+ }
8136
8253
  function loadBody(options) {
8137
8254
  if (options.bodyFile) {
8138
8255
  return fs20.readFileSync(options.bodyFile, "utf8");
@@ -8186,6 +8303,29 @@ function normalizeTaskPriority(priority) {
8186
8303
  function renderTaskPreview(task, body) {
8187
8304
  return stringifyFrontmatter5(task, body);
8188
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
+ }
8189
8329
  function updateTask(id, options) {
8190
8330
  const root = resolveRepoRoot();
8191
8331
  const { filePath, parsed } = loadTaskEntry(root, id);
@@ -8201,16 +8341,19 @@ function updateTask(id, options) {
8201
8341
  if (options.delivery !== void 0) next.delivery = options.delivery.trim() || null;
8202
8342
  if (options.storyPoints !== void 0) next.story_points = Number(options.storyPoints);
8203
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);
8204
8346
  next = {
8205
8347
  ...next,
8206
- delivery_tracks: addValues(removeValues(next.delivery_tracks, options.removeDeliveryTrack), options.addDeliveryTrack),
8207
- depends_on: addValues(removeValues(next.depends_on, options.removeDep), options.addDep),
8208
- tags: addValues(removeValues(next.tags, options.removeTag), options.addTag),
8209
- fix_versions: addValues(removeValues(next.fix_versions, options.removeFixVersion), options.addFixVersion),
8210
- released_in: addValues(removeValues(next.released_in, options.removeReleasedIn), options.addReleasedIn),
8211
- acceptance: addValues(removeValues(next.acceptance, options.acceptanceRemove), options.acceptanceAdd),
8212
- 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)
8213
8355
  };
8356
+ next = normalizeOrigin(next, nextAuthorityRefs, nextDerivedRefs);
8214
8357
  next = clearTrackPriorityOverrides(applyTrackPriorityOverrides(next, options.priorityIn), options.clearPriorityIn);
8215
8358
  next = validateTaskForWrite(root, next, filePath);
8216
8359
  const nextBody = loadBody(options) ?? parsed.body;
@@ -8232,8 +8375,8 @@ function updateIdea(id, options) {
8232
8375
  ...parsed.idea,
8233
8376
  title: options.title?.trim() || parsed.idea.title,
8234
8377
  status: nextStatus || parsed.idea.status,
8235
- tags: addValues(removeValues(parsed.idea.tags, options.removeTag), options.addTag) ?? [],
8236
- 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)) ?? []
8237
8380
  };
8238
8381
  const nextBody = loadBody(options) ?? parsed.body;
8239
8382
  if (options.dryRun) {
@@ -8244,7 +8387,7 @@ function updateIdea(id, options) {
8244
8387
  console.log(`Updated ${next.id}`);
8245
8388
  }
8246
8389
  function registerUpdateCommand(program) {
8247
- 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) => {
8248
8391
  let resolved = resolveOptionalEntityArg(first, second, ["task", "idea"], "task");
8249
8392
  if (!second && resolved.entity === "task") {
8250
8393
  try {
@@ -8799,10 +8942,14 @@ function renderBasicHelp() {
8799
8942
  "- `coop naming`: inspect per-entity ID rules and naming tokens",
8800
8943
  '- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item; templates support `TITLE##` like `TITLE18`, `TITLE8`, or `TITLE08`',
8801
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",
8802
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",
8803
8948
  "- `coop show <id>`: inspect a task, idea, or delivery",
8804
8949
  "- `coop list tasks --track <id>`: browse scoped work",
8805
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",
8806
8953
  '- `coop comment <id> --message "..."`: append a task comment',
8807
8954
  "- `coop log-time <id> --hours 2 --kind worked`: append time spent",
8808
8955
  "- `coop alias remove <id> <alias>`: remove a shorthand alias from a task or idea",
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",
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.4",
43
- "@kitsy/coop-core": "2.2.4",
44
- "@kitsy/coop-ui": "^2.2.4"
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",