@kitsy/coop-ai 2.0.0 → 2.1.0

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.cjs CHANGED
@@ -30,18 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ buildIdeaRefinementPrompt: () => buildIdeaRefinementPrompt,
34
+ buildTaskRefinementPrompt: () => buildTaskRefinementPrompt,
33
35
  build_contract: () => build_contract,
34
36
  build_decomposition_prompt: () => build_decomposition_prompt,
35
37
  constraint_violation_reasons: () => constraint_violation_reasons,
36
38
  create_provider: () => create_provider,
37
39
  create_provider_agent_client: () => create_provider_agent_client,
38
40
  create_provider_idea_decomposer: () => create_provider_idea_decomposer,
41
+ create_provider_refinement_client: () => create_provider_refinement_client,
39
42
  create_run: () => create_run,
40
43
  decompose_idea_to_tasks: () => decompose_idea_to_tasks,
41
44
  enforce_constraints: () => enforce_constraints,
42
45
  execute_task: () => execute_task,
43
46
  finalize_run: () => finalize_run,
44
47
  log_step: () => log_step,
48
+ parseRefinementDraftResponse: () => parseRefinementDraftResponse,
49
+ refine_idea_to_draft: () => refine_idea_to_draft,
50
+ refine_task_to_draft: () => refine_task_to_draft,
45
51
  resolve_provider_config: () => resolve_provider_config,
46
52
  select_agent: () => select_agent,
47
53
  validate_command: () => validate_command,
@@ -101,6 +107,17 @@ function taskAcceptance(task) {
101
107
  const maybe = asRecord(task).acceptance;
102
108
  return asStringArray(maybe) ?? [];
103
109
  }
110
+ function taskTestsRequired(task) {
111
+ const maybe = asRecord(task).tests_required;
112
+ return asStringArray(maybe) ?? [];
113
+ }
114
+ function taskOriginRefs(task) {
115
+ const origin = asRecord(asRecord(task).origin);
116
+ return {
117
+ authority_refs: asStringArray(origin.authority_refs) ?? [],
118
+ derived_refs: asStringArray(origin.derived_refs) ?? []
119
+ };
120
+ }
104
121
  function collectRelatedArtifacts(task, graph) {
105
122
  const context = asRecord(task.execution?.context ?? {});
106
123
  const relatedTaskIds = dedupe(asStringArray(context.tasks) ?? []);
@@ -195,9 +212,12 @@ function build_contract(task, graph, config) {
195
212
  const taskContextFiles = asStringArray(executionContext.files) ?? [];
196
213
  const taskContextTasks = asStringArray(executionContext.tasks) ?? [];
197
214
  const acceptance_criteria = taskAcceptance(task);
215
+ const tests_required = taskTestsRequired(task);
216
+ const origin_refs = taskOriginRefs(task);
198
217
  const related_task_artifacts = collectRelatedArtifacts(task, graph);
199
218
  const output_requirements = [
200
219
  ...DEFAULT_OUTPUT_REQUIREMENTS,
220
+ ...tests_required.length > 0 ? tests_required.map((entry) => `Required test: ${entry}`) : [],
201
221
  ...(task.artifacts?.produces ?? []).map((artifact) => {
202
222
  if (artifact.path) {
203
223
  return `Produce ${artifact.type} artifact at ${artifact.path}`;
@@ -229,6 +249,9 @@ function build_contract(task, graph, config) {
229
249
  files: taskContextFiles,
230
250
  tasks: taskContextTasks,
231
251
  acceptance_criteria,
252
+ tests_required,
253
+ authority_refs: origin_refs.authority_refs,
254
+ derived_refs: origin_refs.derived_refs,
232
255
  related_task_artifacts
233
256
  },
234
257
  output_requirements
@@ -628,7 +651,7 @@ function build_decomposition_prompt(input) {
628
651
  return [
629
652
  "You are a planning agent for COOP.",
630
653
  "Decompose the idea into 2-5 implementation tasks.",
631
- "Each task needs: title, type(feature|bug|chore|spike), priority(p0-p3), and body.",
654
+ "Each task needs: title, type(feature|bug|chore|spike), priority(p0-p3), body, acceptance_criteria, and tests_required.",
632
655
  `Idea ID: ${input.idea_id}`,
633
656
  `Title: ${input.title}`,
634
657
  "Body:",
@@ -647,6 +670,259 @@ async function decompose_idea_to_tasks(input, client) {
647
670
  return drafts;
648
671
  }
649
672
 
673
+ // src/refinement/refine.ts
674
+ function sentenceCase2(value) {
675
+ if (value.length === 0) return value;
676
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
677
+ }
678
+ function nonEmptyLines2(input) {
679
+ return input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
680
+ }
681
+ function extractBulletIdeas2(body) {
682
+ return nonEmptyLines2(body).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter(Boolean).slice(0, 5);
683
+ }
684
+ function taskBodySections(title) {
685
+ return [
686
+ "## Objective",
687
+ title,
688
+ "",
689
+ "## Scope",
690
+ "- Define implementation boundaries",
691
+ "",
692
+ "## Constraints",
693
+ "- Preserve existing behavior unless explicitly changed",
694
+ "",
695
+ "## References",
696
+ "- Add source refs during refinement/apply",
697
+ "",
698
+ "## Refinement Notes",
699
+ "- Generated by COOP refinement fallback"
700
+ ].join("\n");
701
+ }
702
+ function normalizeStringArray(value) {
703
+ if (!Array.isArray(value)) return void 0;
704
+ const entries = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
705
+ return entries.length > 0 ? entries : void 0;
706
+ }
707
+ function normalizeProposal(value) {
708
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
709
+ return null;
710
+ }
711
+ const record = value;
712
+ const title = typeof record.title === "string" ? record.title.trim() : "";
713
+ if (!title) return null;
714
+ const action = record.action === "update" ? "update" : "create";
715
+ return {
716
+ action,
717
+ id: typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0,
718
+ target_id: typeof record.target_id === "string" && record.target_id.trim() ? record.target_id.trim() : void 0,
719
+ title,
720
+ type: record.type === "feature" || record.type === "bug" || record.type === "chore" || record.type === "spike" || record.type === "epic" ? record.type : void 0,
721
+ 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,
722
+ track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
723
+ priority: record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0,
724
+ depends_on: normalizeStringArray(record.depends_on),
725
+ acceptance: normalizeStringArray(record.acceptance),
726
+ tests_required: normalizeStringArray(record.tests_required),
727
+ authority_refs: normalizeStringArray(record.authority_refs),
728
+ derived_refs: normalizeStringArray(record.derived_refs),
729
+ body: typeof record.body === "string" ? record.body : void 0
730
+ };
731
+ }
732
+ function normalizeDraft(value, mode, sourceId, sourceTitle) {
733
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
734
+ return null;
735
+ }
736
+ const record = value;
737
+ const proposalsRaw = Array.isArray(record.proposals) ? record.proposals : Array.isArray(record.tasks) ? record.tasks : [];
738
+ const proposals = proposalsRaw.map((entry) => normalizeProposal(entry)).filter((entry) => Boolean(entry));
739
+ if (proposals.length === 0) return null;
740
+ const summary = typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : `Refined ${mode} ${sourceId}`;
741
+ return {
742
+ kind: "refinement_draft",
743
+ version: 1,
744
+ mode,
745
+ source: {
746
+ entity_type: mode,
747
+ id: sourceId,
748
+ title: sourceTitle
749
+ },
750
+ summary,
751
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
752
+ proposals
753
+ };
754
+ }
755
+ function parseJsonObject(text) {
756
+ const trimmed = text.trim();
757
+ const candidates = [trimmed];
758
+ const fenced = trimmed.match(/```json\s*([\s\S]*?)```/i);
759
+ if (fenced?.[1]) {
760
+ candidates.push(fenced[1]);
761
+ }
762
+ const start = trimmed.indexOf("{");
763
+ const end = trimmed.lastIndexOf("}");
764
+ if (start >= 0 && end > start) {
765
+ candidates.push(trimmed.slice(start, end + 1));
766
+ }
767
+ for (const candidate of candidates) {
768
+ try {
769
+ const parsed = JSON.parse(candidate);
770
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
771
+ return parsed;
772
+ }
773
+ } catch {
774
+ }
775
+ }
776
+ return null;
777
+ }
778
+ function fallbackIdeaRefinement(input) {
779
+ const bullets = extractBulletIdeas2(input.body);
780
+ const tasks = bullets.length > 0 ? bullets.slice(0, 3).map((bullet, index) => ({
781
+ title: sentenceCase2(bullet),
782
+ body: taskBodySections(sentenceCase2(bullet)),
783
+ type: index === 0 ? "spike" : "feature",
784
+ priority: index === 0 ? "p1" : "p2",
785
+ track: "unassigned",
786
+ acceptance_criteria: [`${sentenceCase2(bullet)} is implemented as described`],
787
+ tests_required: ["Relevant integration or regression coverage is added"]
788
+ })) : [
789
+ {
790
+ title: `Define scope and acceptance for ${input.title}`,
791
+ body: taskBodySections(`Define scope and acceptance for ${input.title}`),
792
+ type: "spike",
793
+ priority: "p1",
794
+ track: "unassigned",
795
+ acceptance_criteria: [`Scope and acceptance are explicit for ${input.title}`],
796
+ tests_required: ["Planning review recorded"]
797
+ },
798
+ {
799
+ title: `Implement ${input.title}`,
800
+ body: taskBodySections(`Implement ${input.title}`),
801
+ type: "feature",
802
+ priority: "p1",
803
+ track: "unassigned",
804
+ acceptance_criteria: [`${input.title} is implemented end-to-end`],
805
+ tests_required: ["Automated test coverage added for the primary path"]
806
+ }
807
+ ];
808
+ return {
809
+ kind: "refinement_draft",
810
+ version: 1,
811
+ mode: "idea",
812
+ source: {
813
+ entity_type: "idea",
814
+ id: input.idea_id,
815
+ title: input.title
816
+ },
817
+ summary: `Refined idea ${input.idea_id} into ${tasks.length} proposed task(s).`,
818
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
819
+ proposals: tasks.map((task) => ({
820
+ action: "create",
821
+ title: task.title,
822
+ type: task.type,
823
+ status: "todo",
824
+ track: task.track ?? "unassigned",
825
+ priority: task.priority,
826
+ acceptance: task.acceptance_criteria,
827
+ tests_required: task.tests_required,
828
+ body: task.body
829
+ }))
830
+ };
831
+ }
832
+ function fallbackTaskRefinement(input) {
833
+ const authorityRefs = input.task.origin?.authority_refs ?? [];
834
+ const derivedRefs = input.task.origin?.derived_refs ?? [];
835
+ const acceptance = input.task.acceptance && input.task.acceptance.length > 0 ? input.task.acceptance : [`${input.task.title} is complete and reviewable against its defined scope`];
836
+ const testsRequired = input.task.tests_required && input.task.tests_required.length > 0 ? input.task.tests_required : ["Automated regression coverage for the changed behavior"];
837
+ return {
838
+ kind: "refinement_draft",
839
+ version: 1,
840
+ mode: "task",
841
+ source: {
842
+ entity_type: "task",
843
+ id: input.task.id,
844
+ title: input.task.title
845
+ },
846
+ summary: `Refined task ${input.task.id} into an execution-ready update proposal.`,
847
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
848
+ proposals: [
849
+ {
850
+ action: "update",
851
+ target_id: input.task.id,
852
+ title: input.task.title,
853
+ type: input.task.type,
854
+ status: input.task.status,
855
+ track: input.task.track,
856
+ priority: input.task.priority,
857
+ depends_on: input.task.depends_on,
858
+ acceptance,
859
+ tests_required: testsRequired,
860
+ authority_refs: authorityRefs,
861
+ derived_refs: derivedRefs,
862
+ body: input.body?.trim() || taskBodySections(input.task.title)
863
+ }
864
+ ]
865
+ };
866
+ }
867
+ function buildIdeaRefinementPrompt(input) {
868
+ return [
869
+ "You are a COOP planning agent.",
870
+ "Return ONLY JSON object with keys: summary, proposals.",
871
+ "proposals must be an array of task proposals.",
872
+ "Each proposal must include: action(create), title, type(feature|bug|chore|spike), priority(p0-p3), track, acceptance(string[]), tests_required(string[]), body.",
873
+ "Use depends_on only for existing task ids when necessary.",
874
+ `Idea ID: ${input.idea_id}`,
875
+ `Title: ${input.title}`,
876
+ "Body:",
877
+ input.body || "(empty)",
878
+ input.supplemental_context?.trim() ? ["Supplemental Context:", input.supplemental_context].join("\n") : ""
879
+ ].filter(Boolean).join("\n");
880
+ }
881
+ function buildTaskRefinementPrompt(input) {
882
+ const authorityContext = (input.authority_context ?? []).map((entry) => `Ref: ${entry.ref}
883
+ ${entry.content}`).join("\n\n---\n\n");
884
+ return [
885
+ "You are a COOP planning/refinement agent.",
886
+ "Return ONLY JSON object with keys: summary, proposals.",
887
+ "proposals must be an array with one update proposal for the current task.",
888
+ "The proposal must include: action(update), target_id, title, type, status, track, priority, depends_on, acceptance(string[]), tests_required(string[]), authority_refs(string[]), derived_refs(string[]), body.",
889
+ `Task ID: ${input.task.id}`,
890
+ `Title: ${input.task.title}`,
891
+ `Status: ${input.task.status}`,
892
+ `Type: ${input.task.type}`,
893
+ `Priority: ${input.task.priority ?? "p2"}`,
894
+ `Track: ${input.task.track ?? "unassigned"}`,
895
+ `Depends On: ${(input.task.depends_on ?? []).join(", ") || "-"}`,
896
+ "Task Body:",
897
+ input.body || "(empty)",
898
+ authorityContext ? ["Authority Context:", authorityContext].join("\n") : "",
899
+ input.supplemental_context?.trim() ? ["Supplemental Context:", input.supplemental_context].join("\n") : ""
900
+ ].filter(Boolean).join("\n");
901
+ }
902
+ async function refine_idea_to_draft(input, client) {
903
+ const prompt = buildIdeaRefinementPrompt(input);
904
+ if (!client) {
905
+ return fallbackIdeaRefinement(input);
906
+ }
907
+ const result = await client.refineIdea(prompt, input);
908
+ return result ?? fallbackIdeaRefinement(input);
909
+ }
910
+ async function refine_task_to_draft(input, client) {
911
+ const prompt = buildTaskRefinementPrompt(input);
912
+ if (!client) {
913
+ return fallbackTaskRefinement(input);
914
+ }
915
+ const result = await client.refineTask(prompt, input);
916
+ return result ?? fallbackTaskRefinement(input);
917
+ }
918
+ function parseRefinementDraftResponse(text, mode, sourceId, sourceTitle) {
919
+ const parsed = parseJsonObject(text);
920
+ if (!parsed) {
921
+ return null;
922
+ }
923
+ return normalizeDraft(parsed, mode, sourceId, sourceTitle);
924
+ }
925
+
650
926
  // src/providers/config.ts
651
927
  var DEFAULT_MODELS = {
652
928
  openai: "gpt-5-mini",
@@ -978,6 +1254,12 @@ function toTaskDrafts(value) {
978
1254
  }
979
1255
  return out;
980
1256
  }
1257
+ function refinementSystemPrompt(mode) {
1258
+ if (mode === "idea") {
1259
+ return "Return ONLY JSON object with keys summary and proposals. proposals must be an array of task proposals with action(create), title, type, priority, track, acceptance, tests_required, body.";
1260
+ }
1261
+ return "Return ONLY JSON object with keys summary and proposals. proposals must be an array with one update proposal containing action(update), target_id, title, type, status, track, priority, depends_on, acceptance, tests_required, authority_refs, derived_refs, body.";
1262
+ }
981
1263
  function asAgentResponse(text, tokens) {
982
1264
  return {
983
1265
  summary: text.trim(),
@@ -1019,20 +1301,45 @@ function create_provider_idea_decomposer(config) {
1019
1301
  }
1020
1302
  };
1021
1303
  }
1304
+ function create_provider_refinement_client(config) {
1305
+ const provider = create_provider(config);
1306
+ return {
1307
+ async refineIdea(prompt, input) {
1308
+ const result = await provider.complete({
1309
+ system: refinementSystemPrompt("idea"),
1310
+ prompt
1311
+ });
1312
+ return parseRefinementDraftResponse(result.text, "idea", input.idea_id, input.title);
1313
+ },
1314
+ async refineTask(prompt, input) {
1315
+ const result = await provider.complete({
1316
+ system: refinementSystemPrompt("task"),
1317
+ prompt
1318
+ });
1319
+ return parseRefinementDraftResponse(result.text, "task", input.task.id, input.task.title);
1320
+ }
1321
+ };
1322
+ }
1022
1323
  // Annotate the CommonJS export names for ESM import in node:
1023
1324
  0 && (module.exports = {
1325
+ buildIdeaRefinementPrompt,
1326
+ buildTaskRefinementPrompt,
1024
1327
  build_contract,
1025
1328
  build_decomposition_prompt,
1026
1329
  constraint_violation_reasons,
1027
1330
  create_provider,
1028
1331
  create_provider_agent_client,
1029
1332
  create_provider_idea_decomposer,
1333
+ create_provider_refinement_client,
1030
1334
  create_run,
1031
1335
  decompose_idea_to_tasks,
1032
1336
  enforce_constraints,
1033
1337
  execute_task,
1034
1338
  finalize_run,
1035
1339
  log_step,
1340
+ parseRefinementDraftResponse,
1341
+ refine_idea_to_draft,
1342
+ refine_task_to_draft,
1036
1343
  resolve_provider_config,
1037
1344
  select_agent,
1038
1345
  validate_command,
package/dist/index.d.cts CHANGED
@@ -25,6 +25,9 @@ interface AgentContractContext {
25
25
  files: string[];
26
26
  tasks: string[];
27
27
  acceptance_criteria: string[];
28
+ tests_required: string[];
29
+ authority_refs: string[];
30
+ derived_refs: string[];
28
31
  related_task_artifacts: AgentContractRelatedArtifacts[];
29
32
  }
30
33
  interface AgentContract {
@@ -141,6 +144,8 @@ interface DecomposedTaskDraft {
141
144
  type?: "feature" | "bug" | "chore" | "spike";
142
145
  priority?: "p0" | "p1" | "p2" | "p3";
143
146
  track?: string;
147
+ acceptance_criteria?: string[];
148
+ tests_required?: string[];
144
149
  }
145
150
  interface IdeaDecomposerClient {
146
151
  decompose(prompt: string, input: IdeaDecompositionInput): Promise<DecomposedTaskDraft[]>;
@@ -156,6 +161,61 @@ declare function build_decomposition_prompt(input: IdeaDecompositionInput): stri
156
161
  */
157
162
  declare function decompose_idea_to_tasks(input: IdeaDecompositionInput, client?: IdeaDecomposerClient): Promise<DecomposedTaskDraft[]>;
158
163
 
164
+ type RefinementProposalAction = "create" | "update";
165
+ interface RefinementTaskProposal {
166
+ action: RefinementProposalAction;
167
+ id?: string;
168
+ target_id?: string;
169
+ title: string;
170
+ type?: Task["type"];
171
+ status?: Task["status"];
172
+ track?: string;
173
+ priority?: "p0" | "p1" | "p2" | "p3";
174
+ depends_on?: string[];
175
+ acceptance?: string[];
176
+ tests_required?: string[];
177
+ authority_refs?: string[];
178
+ derived_refs?: string[];
179
+ body?: string;
180
+ }
181
+ interface RefinementDraft {
182
+ kind: "refinement_draft";
183
+ version: 1;
184
+ mode: "idea" | "task";
185
+ source: {
186
+ entity_type: "idea" | "task";
187
+ id: string;
188
+ title: string;
189
+ };
190
+ summary: string;
191
+ generated_at: string;
192
+ proposals: RefinementTaskProposal[];
193
+ }
194
+ interface IdeaRefinementInput {
195
+ idea_id: string;
196
+ title: string;
197
+ body: string;
198
+ supplemental_context?: string;
199
+ }
200
+ interface TaskRefinementInput {
201
+ task: Task;
202
+ body: string;
203
+ supplemental_context?: string;
204
+ authority_context?: Array<{
205
+ ref: string;
206
+ content: string;
207
+ }>;
208
+ }
209
+ interface RefinementClient {
210
+ refineIdea(prompt: string, input: IdeaRefinementInput): Promise<RefinementDraft | null>;
211
+ refineTask(prompt: string, input: TaskRefinementInput): Promise<RefinementDraft | null>;
212
+ }
213
+ declare function buildIdeaRefinementPrompt(input: IdeaRefinementInput): string;
214
+ declare function buildTaskRefinementPrompt(input: TaskRefinementInput): string;
215
+ declare function refine_idea_to_draft(input: IdeaRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
216
+ declare function refine_task_to_draft(input: TaskRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
217
+ declare function parseRefinementDraftResponse(text: string, mode: "idea" | "task", sourceId: string, sourceTitle: string): RefinementDraft | null;
218
+
159
219
  type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama";
160
220
  interface ProviderConfig {
161
221
  provider: AiProviderName;
@@ -201,5 +261,6 @@ declare function create_provider_agent_client(config: unknown): AgentClient;
201
261
  * [SPEC: Architecture v2.0 §Phase 4]
202
262
  */
203
263
  declare function create_provider_idea_decomposer(config: unknown): IdeaDecomposerClient;
264
+ declare function create_provider_refinement_client(config: unknown): RefinementClient;
204
265
 
205
- export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type LlmProvider, type ProviderConfig, type RunResult, type SandboxRunState, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
266
+ export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
package/dist/index.d.ts CHANGED
@@ -25,6 +25,9 @@ interface AgentContractContext {
25
25
  files: string[];
26
26
  tasks: string[];
27
27
  acceptance_criteria: string[];
28
+ tests_required: string[];
29
+ authority_refs: string[];
30
+ derived_refs: string[];
28
31
  related_task_artifacts: AgentContractRelatedArtifacts[];
29
32
  }
30
33
  interface AgentContract {
@@ -141,6 +144,8 @@ interface DecomposedTaskDraft {
141
144
  type?: "feature" | "bug" | "chore" | "spike";
142
145
  priority?: "p0" | "p1" | "p2" | "p3";
143
146
  track?: string;
147
+ acceptance_criteria?: string[];
148
+ tests_required?: string[];
144
149
  }
145
150
  interface IdeaDecomposerClient {
146
151
  decompose(prompt: string, input: IdeaDecompositionInput): Promise<DecomposedTaskDraft[]>;
@@ -156,6 +161,61 @@ declare function build_decomposition_prompt(input: IdeaDecompositionInput): stri
156
161
  */
157
162
  declare function decompose_idea_to_tasks(input: IdeaDecompositionInput, client?: IdeaDecomposerClient): Promise<DecomposedTaskDraft[]>;
158
163
 
164
+ type RefinementProposalAction = "create" | "update";
165
+ interface RefinementTaskProposal {
166
+ action: RefinementProposalAction;
167
+ id?: string;
168
+ target_id?: string;
169
+ title: string;
170
+ type?: Task["type"];
171
+ status?: Task["status"];
172
+ track?: string;
173
+ priority?: "p0" | "p1" | "p2" | "p3";
174
+ depends_on?: string[];
175
+ acceptance?: string[];
176
+ tests_required?: string[];
177
+ authority_refs?: string[];
178
+ derived_refs?: string[];
179
+ body?: string;
180
+ }
181
+ interface RefinementDraft {
182
+ kind: "refinement_draft";
183
+ version: 1;
184
+ mode: "idea" | "task";
185
+ source: {
186
+ entity_type: "idea" | "task";
187
+ id: string;
188
+ title: string;
189
+ };
190
+ summary: string;
191
+ generated_at: string;
192
+ proposals: RefinementTaskProposal[];
193
+ }
194
+ interface IdeaRefinementInput {
195
+ idea_id: string;
196
+ title: string;
197
+ body: string;
198
+ supplemental_context?: string;
199
+ }
200
+ interface TaskRefinementInput {
201
+ task: Task;
202
+ body: string;
203
+ supplemental_context?: string;
204
+ authority_context?: Array<{
205
+ ref: string;
206
+ content: string;
207
+ }>;
208
+ }
209
+ interface RefinementClient {
210
+ refineIdea(prompt: string, input: IdeaRefinementInput): Promise<RefinementDraft | null>;
211
+ refineTask(prompt: string, input: TaskRefinementInput): Promise<RefinementDraft | null>;
212
+ }
213
+ declare function buildIdeaRefinementPrompt(input: IdeaRefinementInput): string;
214
+ declare function buildTaskRefinementPrompt(input: TaskRefinementInput): string;
215
+ declare function refine_idea_to_draft(input: IdeaRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
216
+ declare function refine_task_to_draft(input: TaskRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
217
+ declare function parseRefinementDraftResponse(text: string, mode: "idea" | "task", sourceId: string, sourceTitle: string): RefinementDraft | null;
218
+
159
219
  type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama";
160
220
  interface ProviderConfig {
161
221
  provider: AiProviderName;
@@ -201,5 +261,6 @@ declare function create_provider_agent_client(config: unknown): AgentClient;
201
261
  * [SPEC: Architecture v2.0 §Phase 4]
202
262
  */
203
263
  declare function create_provider_idea_decomposer(config: unknown): IdeaDecomposerClient;
264
+ declare function create_provider_refinement_client(config: unknown): RefinementClient;
204
265
 
205
- export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type LlmProvider, type ProviderConfig, type RunResult, type SandboxRunState, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
266
+ export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
package/dist/index.js CHANGED
@@ -49,6 +49,17 @@ function taskAcceptance(task) {
49
49
  const maybe = asRecord(task).acceptance;
50
50
  return asStringArray(maybe) ?? [];
51
51
  }
52
+ function taskTestsRequired(task) {
53
+ const maybe = asRecord(task).tests_required;
54
+ return asStringArray(maybe) ?? [];
55
+ }
56
+ function taskOriginRefs(task) {
57
+ const origin = asRecord(asRecord(task).origin);
58
+ return {
59
+ authority_refs: asStringArray(origin.authority_refs) ?? [],
60
+ derived_refs: asStringArray(origin.derived_refs) ?? []
61
+ };
62
+ }
52
63
  function collectRelatedArtifacts(task, graph) {
53
64
  const context = asRecord(task.execution?.context ?? {});
54
65
  const relatedTaskIds = dedupe(asStringArray(context.tasks) ?? []);
@@ -143,9 +154,12 @@ function build_contract(task, graph, config) {
143
154
  const taskContextFiles = asStringArray(executionContext.files) ?? [];
144
155
  const taskContextTasks = asStringArray(executionContext.tasks) ?? [];
145
156
  const acceptance_criteria = taskAcceptance(task);
157
+ const tests_required = taskTestsRequired(task);
158
+ const origin_refs = taskOriginRefs(task);
146
159
  const related_task_artifacts = collectRelatedArtifacts(task, graph);
147
160
  const output_requirements = [
148
161
  ...DEFAULT_OUTPUT_REQUIREMENTS,
162
+ ...tests_required.length > 0 ? tests_required.map((entry) => `Required test: ${entry}`) : [],
149
163
  ...(task.artifacts?.produces ?? []).map((artifact) => {
150
164
  if (artifact.path) {
151
165
  return `Produce ${artifact.type} artifact at ${artifact.path}`;
@@ -177,6 +191,9 @@ function build_contract(task, graph, config) {
177
191
  files: taskContextFiles,
178
192
  tasks: taskContextTasks,
179
193
  acceptance_criteria,
194
+ tests_required,
195
+ authority_refs: origin_refs.authority_refs,
196
+ derived_refs: origin_refs.derived_refs,
180
197
  related_task_artifacts
181
198
  },
182
199
  output_requirements
@@ -576,7 +593,7 @@ function build_decomposition_prompt(input) {
576
593
  return [
577
594
  "You are a planning agent for COOP.",
578
595
  "Decompose the idea into 2-5 implementation tasks.",
579
- "Each task needs: title, type(feature|bug|chore|spike), priority(p0-p3), and body.",
596
+ "Each task needs: title, type(feature|bug|chore|spike), priority(p0-p3), body, acceptance_criteria, and tests_required.",
580
597
  `Idea ID: ${input.idea_id}`,
581
598
  `Title: ${input.title}`,
582
599
  "Body:",
@@ -595,6 +612,259 @@ async function decompose_idea_to_tasks(input, client) {
595
612
  return drafts;
596
613
  }
597
614
 
615
+ // src/refinement/refine.ts
616
+ function sentenceCase2(value) {
617
+ if (value.length === 0) return value;
618
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
619
+ }
620
+ function nonEmptyLines2(input) {
621
+ return input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
622
+ }
623
+ function extractBulletIdeas2(body) {
624
+ return nonEmptyLines2(body).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter(Boolean).slice(0, 5);
625
+ }
626
+ function taskBodySections(title) {
627
+ return [
628
+ "## Objective",
629
+ title,
630
+ "",
631
+ "## Scope",
632
+ "- Define implementation boundaries",
633
+ "",
634
+ "## Constraints",
635
+ "- Preserve existing behavior unless explicitly changed",
636
+ "",
637
+ "## References",
638
+ "- Add source refs during refinement/apply",
639
+ "",
640
+ "## Refinement Notes",
641
+ "- Generated by COOP refinement fallback"
642
+ ].join("\n");
643
+ }
644
+ function normalizeStringArray(value) {
645
+ if (!Array.isArray(value)) return void 0;
646
+ const entries = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
647
+ return entries.length > 0 ? entries : void 0;
648
+ }
649
+ function normalizeProposal(value) {
650
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
651
+ return null;
652
+ }
653
+ const record = value;
654
+ const title = typeof record.title === "string" ? record.title.trim() : "";
655
+ if (!title) return null;
656
+ const action = record.action === "update" ? "update" : "create";
657
+ return {
658
+ action,
659
+ id: typeof record.id === "string" && record.id.trim() ? record.id.trim() : void 0,
660
+ target_id: typeof record.target_id === "string" && record.target_id.trim() ? record.target_id.trim() : void 0,
661
+ title,
662
+ type: record.type === "feature" || record.type === "bug" || record.type === "chore" || record.type === "spike" || record.type === "epic" ? record.type : void 0,
663
+ 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,
664
+ track: typeof record.track === "string" ? record.track.trim() || void 0 : void 0,
665
+ priority: record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0,
666
+ depends_on: normalizeStringArray(record.depends_on),
667
+ acceptance: normalizeStringArray(record.acceptance),
668
+ tests_required: normalizeStringArray(record.tests_required),
669
+ authority_refs: normalizeStringArray(record.authority_refs),
670
+ derived_refs: normalizeStringArray(record.derived_refs),
671
+ body: typeof record.body === "string" ? record.body : void 0
672
+ };
673
+ }
674
+ function normalizeDraft(value, mode, sourceId, sourceTitle) {
675
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
676
+ return null;
677
+ }
678
+ const record = value;
679
+ const proposalsRaw = Array.isArray(record.proposals) ? record.proposals : Array.isArray(record.tasks) ? record.tasks : [];
680
+ const proposals = proposalsRaw.map((entry) => normalizeProposal(entry)).filter((entry) => Boolean(entry));
681
+ if (proposals.length === 0) return null;
682
+ const summary = typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : `Refined ${mode} ${sourceId}`;
683
+ return {
684
+ kind: "refinement_draft",
685
+ version: 1,
686
+ mode,
687
+ source: {
688
+ entity_type: mode,
689
+ id: sourceId,
690
+ title: sourceTitle
691
+ },
692
+ summary,
693
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
694
+ proposals
695
+ };
696
+ }
697
+ function parseJsonObject(text) {
698
+ const trimmed = text.trim();
699
+ const candidates = [trimmed];
700
+ const fenced = trimmed.match(/```json\s*([\s\S]*?)```/i);
701
+ if (fenced?.[1]) {
702
+ candidates.push(fenced[1]);
703
+ }
704
+ const start = trimmed.indexOf("{");
705
+ const end = trimmed.lastIndexOf("}");
706
+ if (start >= 0 && end > start) {
707
+ candidates.push(trimmed.slice(start, end + 1));
708
+ }
709
+ for (const candidate of candidates) {
710
+ try {
711
+ const parsed = JSON.parse(candidate);
712
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
713
+ return parsed;
714
+ }
715
+ } catch {
716
+ }
717
+ }
718
+ return null;
719
+ }
720
+ function fallbackIdeaRefinement(input) {
721
+ const bullets = extractBulletIdeas2(input.body);
722
+ const tasks = bullets.length > 0 ? bullets.slice(0, 3).map((bullet, index) => ({
723
+ title: sentenceCase2(bullet),
724
+ body: taskBodySections(sentenceCase2(bullet)),
725
+ type: index === 0 ? "spike" : "feature",
726
+ priority: index === 0 ? "p1" : "p2",
727
+ track: "unassigned",
728
+ acceptance_criteria: [`${sentenceCase2(bullet)} is implemented as described`],
729
+ tests_required: ["Relevant integration or regression coverage is added"]
730
+ })) : [
731
+ {
732
+ title: `Define scope and acceptance for ${input.title}`,
733
+ body: taskBodySections(`Define scope and acceptance for ${input.title}`),
734
+ type: "spike",
735
+ priority: "p1",
736
+ track: "unassigned",
737
+ acceptance_criteria: [`Scope and acceptance are explicit for ${input.title}`],
738
+ tests_required: ["Planning review recorded"]
739
+ },
740
+ {
741
+ title: `Implement ${input.title}`,
742
+ body: taskBodySections(`Implement ${input.title}`),
743
+ type: "feature",
744
+ priority: "p1",
745
+ track: "unassigned",
746
+ acceptance_criteria: [`${input.title} is implemented end-to-end`],
747
+ tests_required: ["Automated test coverage added for the primary path"]
748
+ }
749
+ ];
750
+ return {
751
+ kind: "refinement_draft",
752
+ version: 1,
753
+ mode: "idea",
754
+ source: {
755
+ entity_type: "idea",
756
+ id: input.idea_id,
757
+ title: input.title
758
+ },
759
+ summary: `Refined idea ${input.idea_id} into ${tasks.length} proposed task(s).`,
760
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
761
+ proposals: tasks.map((task) => ({
762
+ action: "create",
763
+ title: task.title,
764
+ type: task.type,
765
+ status: "todo",
766
+ track: task.track ?? "unassigned",
767
+ priority: task.priority,
768
+ acceptance: task.acceptance_criteria,
769
+ tests_required: task.tests_required,
770
+ body: task.body
771
+ }))
772
+ };
773
+ }
774
+ function fallbackTaskRefinement(input) {
775
+ const authorityRefs = input.task.origin?.authority_refs ?? [];
776
+ const derivedRefs = input.task.origin?.derived_refs ?? [];
777
+ const acceptance = input.task.acceptance && input.task.acceptance.length > 0 ? input.task.acceptance : [`${input.task.title} is complete and reviewable against its defined scope`];
778
+ const testsRequired = input.task.tests_required && input.task.tests_required.length > 0 ? input.task.tests_required : ["Automated regression coverage for the changed behavior"];
779
+ return {
780
+ kind: "refinement_draft",
781
+ version: 1,
782
+ mode: "task",
783
+ source: {
784
+ entity_type: "task",
785
+ id: input.task.id,
786
+ title: input.task.title
787
+ },
788
+ summary: `Refined task ${input.task.id} into an execution-ready update proposal.`,
789
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
790
+ proposals: [
791
+ {
792
+ action: "update",
793
+ target_id: input.task.id,
794
+ title: input.task.title,
795
+ type: input.task.type,
796
+ status: input.task.status,
797
+ track: input.task.track,
798
+ priority: input.task.priority,
799
+ depends_on: input.task.depends_on,
800
+ acceptance,
801
+ tests_required: testsRequired,
802
+ authority_refs: authorityRefs,
803
+ derived_refs: derivedRefs,
804
+ body: input.body?.trim() || taskBodySections(input.task.title)
805
+ }
806
+ ]
807
+ };
808
+ }
809
+ function buildIdeaRefinementPrompt(input) {
810
+ return [
811
+ "You are a COOP planning agent.",
812
+ "Return ONLY JSON object with keys: summary, proposals.",
813
+ "proposals must be an array of task proposals.",
814
+ "Each proposal must include: action(create), title, type(feature|bug|chore|spike), priority(p0-p3), track, acceptance(string[]), tests_required(string[]), body.",
815
+ "Use depends_on only for existing task ids when necessary.",
816
+ `Idea ID: ${input.idea_id}`,
817
+ `Title: ${input.title}`,
818
+ "Body:",
819
+ input.body || "(empty)",
820
+ input.supplemental_context?.trim() ? ["Supplemental Context:", input.supplemental_context].join("\n") : ""
821
+ ].filter(Boolean).join("\n");
822
+ }
823
+ function buildTaskRefinementPrompt(input) {
824
+ const authorityContext = (input.authority_context ?? []).map((entry) => `Ref: ${entry.ref}
825
+ ${entry.content}`).join("\n\n---\n\n");
826
+ return [
827
+ "You are a COOP planning/refinement agent.",
828
+ "Return ONLY JSON object with keys: summary, proposals.",
829
+ "proposals must be an array with one update proposal for the current task.",
830
+ "The proposal must include: action(update), target_id, title, type, status, track, priority, depends_on, acceptance(string[]), tests_required(string[]), authority_refs(string[]), derived_refs(string[]), body.",
831
+ `Task ID: ${input.task.id}`,
832
+ `Title: ${input.task.title}`,
833
+ `Status: ${input.task.status}`,
834
+ `Type: ${input.task.type}`,
835
+ `Priority: ${input.task.priority ?? "p2"}`,
836
+ `Track: ${input.task.track ?? "unassigned"}`,
837
+ `Depends On: ${(input.task.depends_on ?? []).join(", ") || "-"}`,
838
+ "Task Body:",
839
+ input.body || "(empty)",
840
+ authorityContext ? ["Authority Context:", authorityContext].join("\n") : "",
841
+ input.supplemental_context?.trim() ? ["Supplemental Context:", input.supplemental_context].join("\n") : ""
842
+ ].filter(Boolean).join("\n");
843
+ }
844
+ async function refine_idea_to_draft(input, client) {
845
+ const prompt = buildIdeaRefinementPrompt(input);
846
+ if (!client) {
847
+ return fallbackIdeaRefinement(input);
848
+ }
849
+ const result = await client.refineIdea(prompt, input);
850
+ return result ?? fallbackIdeaRefinement(input);
851
+ }
852
+ async function refine_task_to_draft(input, client) {
853
+ const prompt = buildTaskRefinementPrompt(input);
854
+ if (!client) {
855
+ return fallbackTaskRefinement(input);
856
+ }
857
+ const result = await client.refineTask(prompt, input);
858
+ return result ?? fallbackTaskRefinement(input);
859
+ }
860
+ function parseRefinementDraftResponse(text, mode, sourceId, sourceTitle) {
861
+ const parsed = parseJsonObject(text);
862
+ if (!parsed) {
863
+ return null;
864
+ }
865
+ return normalizeDraft(parsed, mode, sourceId, sourceTitle);
866
+ }
867
+
598
868
  // src/providers/config.ts
599
869
  var DEFAULT_MODELS = {
600
870
  openai: "gpt-5-mini",
@@ -926,6 +1196,12 @@ function toTaskDrafts(value) {
926
1196
  }
927
1197
  return out;
928
1198
  }
1199
+ function refinementSystemPrompt(mode) {
1200
+ if (mode === "idea") {
1201
+ return "Return ONLY JSON object with keys summary and proposals. proposals must be an array of task proposals with action(create), title, type, priority, track, acceptance, tests_required, body.";
1202
+ }
1203
+ return "Return ONLY JSON object with keys summary and proposals. proposals must be an array with one update proposal containing action(update), target_id, title, type, status, track, priority, depends_on, acceptance, tests_required, authority_refs, derived_refs, body.";
1204
+ }
929
1205
  function asAgentResponse(text, tokens) {
930
1206
  return {
931
1207
  summary: text.trim(),
@@ -967,19 +1243,44 @@ function create_provider_idea_decomposer(config) {
967
1243
  }
968
1244
  };
969
1245
  }
1246
+ function create_provider_refinement_client(config) {
1247
+ const provider = create_provider(config);
1248
+ return {
1249
+ async refineIdea(prompt, input) {
1250
+ const result = await provider.complete({
1251
+ system: refinementSystemPrompt("idea"),
1252
+ prompt
1253
+ });
1254
+ return parseRefinementDraftResponse(result.text, "idea", input.idea_id, input.title);
1255
+ },
1256
+ async refineTask(prompt, input) {
1257
+ const result = await provider.complete({
1258
+ system: refinementSystemPrompt("task"),
1259
+ prompt
1260
+ });
1261
+ return parseRefinementDraftResponse(result.text, "task", input.task.id, input.task.title);
1262
+ }
1263
+ };
1264
+ }
970
1265
  export {
1266
+ buildIdeaRefinementPrompt,
1267
+ buildTaskRefinementPrompt,
971
1268
  build_contract,
972
1269
  build_decomposition_prompt,
973
1270
  constraint_violation_reasons,
974
1271
  create_provider,
975
1272
  create_provider_agent_client,
976
1273
  create_provider_idea_decomposer,
1274
+ create_provider_refinement_client,
977
1275
  create_run,
978
1276
  decompose_idea_to_tasks,
979
1277
  enforce_constraints,
980
1278
  execute_task,
981
1279
  finalize_run,
982
1280
  log_step,
1281
+ parseRefinementDraftResponse,
1282
+ refine_idea_to_draft,
1283
+ refine_task_to_draft,
983
1284
  resolve_provider_config,
984
1285
  select_agent,
985
1286
  validate_command,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitsy/coop-ai",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "dependencies": {
20
- "@kitsy/coop-core": "2.0.0"
20
+ "@kitsy/coop-core": "2.1.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^24.12.0",