@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.
- package/dist/index.js +228 -81
- 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
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
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
|
|
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
|
-
|
|
2702
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3026
|
-
|
|
3027
|
-
...
|
|
3028
|
-
body: body ||
|
|
3110
|
+
const parsed2 = parseTaskDraftObject(frontmatter, source);
|
|
3111
|
+
parsed2.value.proposals[0] = {
|
|
3112
|
+
...parsed2.value.proposals[0],
|
|
3113
|
+
body: body || parsed2.value.proposals[0]?.body
|
|
3029
3114
|
};
|
|
3030
|
-
return
|
|
3115
|
+
return parsed2;
|
|
3031
3116
|
}
|
|
3032
3117
|
const parsed = parseYamlContent2(content, source);
|
|
3033
3118
|
if (parsed.kind === "refinement_draft") {
|
|
3034
|
-
return
|
|
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
|
|
3051
|
-
|
|
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 <
|
|
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,
|
|
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
|
|
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,
|
|
3983
|
-
purpose: "Create a planning-grade task with acceptance
|
|
4080
|
+
usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved, client mapping documented" --acceptance "Rollback path documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
|
|
4081
|
+
purpose: "Create a planning-grade task with repeatable free-text acceptance/tests fields and origin refs."
|
|
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
|
|
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
|
+
"version": "2.2.5",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"commander": "^14.0.0",
|
|
41
41
|
"octokit": "^5.0.5",
|
|
42
|
-
"@kitsy/coop-ai": "2.2.
|
|
43
|
-
"@kitsy/coop-core": "2.2.
|
|
44
|
-
"@kitsy/coop-ui": "^2.2.
|
|
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",
|