@kitsy/coop-ai 0.0.1 → 1.0.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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,19 +17,1025 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
- coopAiRuntime: () => coopAiRuntime
33
+ build_contract: () => build_contract,
34
+ build_decomposition_prompt: () => build_decomposition_prompt,
35
+ constraint_violation_reasons: () => constraint_violation_reasons,
36
+ create_provider: () => create_provider,
37
+ create_provider_agent_client: () => create_provider_agent_client,
38
+ create_provider_idea_decomposer: () => create_provider_idea_decomposer,
39
+ create_run: () => create_run,
40
+ decompose_idea_to_tasks: () => decompose_idea_to_tasks,
41
+ enforce_constraints: () => enforce_constraints,
42
+ execute_task: () => execute_task,
43
+ finalize_run: () => finalize_run,
44
+ log_step: () => log_step,
45
+ resolve_provider_config: () => resolve_provider_config,
46
+ select_agent: () => select_agent,
47
+ validate_command: () => validate_command,
48
+ validate_file_access: () => validate_file_access,
49
+ write_run: () => write_run
24
50
  });
25
51
  module.exports = __toCommonJS(index_exports);
26
- var coopAiRuntime = {
27
- package: "@kitsy/coop-ai",
28
- status: "planned"
52
+
53
+ // src/contracts/contract-builder.ts
54
+ var DEFAULT_FORBIDDEN_COMMANDS = ["rm -rf", "git push", "deploy"];
55
+ var DEFAULT_OUTPUT_REQUIREMENTS = [
56
+ "All changes on a feature branch",
57
+ "PR description with acceptance criteria mapping",
58
+ "Test coverage report"
59
+ ];
60
+ function asRecord(value) {
61
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
62
+ return {};
63
+ }
64
+ return value;
65
+ }
66
+ function asStringArray(value) {
67
+ if (!Array.isArray(value)) return null;
68
+ const out = value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
69
+ return out;
70
+ }
71
+ function asBoolean(value, fallback) {
72
+ if (typeof value === "boolean") return value;
73
+ return fallback;
74
+ }
75
+ function asFiniteNumber(value) {
76
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
77
+ return value;
78
+ }
79
+ function normalizePath(input) {
80
+ const collapsed = input.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/\/$/, "").trim();
81
+ return collapsed.length > 0 ? collapsed : ".";
82
+ }
83
+ function isSubPath(candidate, parent) {
84
+ const pathValue = normalizePath(candidate);
85
+ const parentValue = normalizePath(parent);
86
+ if (parentValue === "." || parentValue === "") return true;
87
+ return pathValue === parentValue || pathValue.startsWith(`${parentValue}/`);
88
+ }
89
+ function normalizeCommand(command) {
90
+ return command.trim().replace(/\s+/g, " ").toLowerCase();
91
+ }
92
+ function commandMatchesRule(command, rule) {
93
+ const normalizedCommand = normalizeCommand(command);
94
+ const normalizedRule = normalizeCommand(rule);
95
+ return normalizedCommand === normalizedRule || normalizedCommand.startsWith(`${normalizedRule} `);
96
+ }
97
+ function dedupe(values) {
98
+ return Array.from(new Set(values));
99
+ }
100
+ function taskAcceptance(task) {
101
+ const maybe = asRecord(task).acceptance;
102
+ return asStringArray(maybe) ?? [];
103
+ }
104
+ function collectRelatedArtifacts(task, graph) {
105
+ const context = asRecord(task.execution?.context ?? {});
106
+ const relatedTaskIds = dedupe(asStringArray(context.tasks) ?? []);
107
+ const artifacts = [];
108
+ for (const taskId of relatedTaskIds) {
109
+ const related = graph.nodes.get(taskId);
110
+ if (!related) continue;
111
+ const produces = related.artifacts?.produces ?? [];
112
+ artifacts.push({
113
+ task_id: related.id,
114
+ artifacts: produces.map((artifact) => ({
115
+ type: artifact.type,
116
+ target: artifact.target,
117
+ path: artifact.path
118
+ }))
119
+ });
120
+ }
121
+ return artifacts;
122
+ }
123
+ function computeGoal(task) {
124
+ const acceptance = taskAcceptance(task);
125
+ if (acceptance.length === 0) {
126
+ return task.title.trim();
127
+ }
128
+ return `${task.title.trim()}. Acceptance criteria: ${acceptance.join("; ")}`;
129
+ }
130
+ function readAiConfig(config) {
131
+ const root = asRecord(config);
132
+ const ai = asRecord(root.ai);
133
+ const permissions = asRecord(ai.permissions);
134
+ const constraints = asRecord(ai.constraints);
135
+ const default_executor = typeof ai.default_executor === "string" && ai.default_executor.trim().length > 0 ? ai.default_executor.trim() : "claude-sonnet";
136
+ const token_budget_per_task = asFiniteNumber(ai.token_budget_per_task) ?? 5e4;
137
+ return {
138
+ default_executor,
139
+ token_budget_per_task,
140
+ permissions,
141
+ constraints
142
+ };
143
+ }
144
+ function validateContract(contract) {
145
+ if (contract.constraints.max_tokens <= 0) {
146
+ throw new Error("Invalid contract: constraints.max_tokens must be positive.");
147
+ }
148
+ for (const writePath of contract.permissions.write_paths) {
149
+ const allowed = contract.permissions.read_paths.some(
150
+ (readPath) => isSubPath(writePath, readPath)
151
+ );
152
+ if (!allowed) {
153
+ throw new Error(
154
+ `Invalid contract: write path '${writePath}' is outside declared read paths.`
155
+ );
156
+ }
157
+ }
158
+ }
159
+ function build_contract(task, graph, config) {
160
+ const ai = readAiConfig(config);
161
+ const execution = task.execution;
162
+ const executionContext = asRecord(execution?.context ?? {});
163
+ const defaultReadPaths = asStringArray(ai.permissions.read_paths) ?? ["."];
164
+ const defaultWritePaths = asStringArray(ai.permissions.write_paths) ?? defaultReadPaths;
165
+ const defaultAllowedCommands = asStringArray(ai.permissions.allowed_commands) ?? [];
166
+ const defaultForbiddenCommands = asStringArray(ai.permissions.forbidden_commands) ?? DEFAULT_FORBIDDEN_COMMANDS;
167
+ const read_paths = dedupe(
168
+ (execution?.permissions?.read_paths ?? defaultReadPaths).map((entry) => normalizePath(entry))
169
+ );
170
+ const write_paths = dedupe(
171
+ (execution?.permissions?.write_paths ?? defaultWritePaths).map((entry) => normalizePath(entry))
172
+ );
173
+ const forbidden_commands = dedupe(
174
+ (execution?.permissions?.forbidden_commands ?? defaultForbiddenCommands).map(
175
+ (entry) => entry.trim()
176
+ )
177
+ );
178
+ const candidate_allowed = dedupe(
179
+ (execution?.permissions?.allowed_commands ?? defaultAllowedCommands).map(
180
+ (entry) => entry.trim()
181
+ )
182
+ );
183
+ const allowed_commands = candidate_allowed.filter(
184
+ (allowed) => !forbidden_commands.some((forbidden) => commandMatchesRule(allowed, forbidden))
185
+ );
186
+ const constraintsConfig = ai.constraints;
187
+ const max_tokens = execution?.constraints?.max_tokens ?? asFiniteNumber(constraintsConfig.max_tokens) ?? ai.token_budget_per_task;
188
+ const max_duration_minutes = execution?.constraints?.max_duration_minutes ?? asFiniteNumber(constraintsConfig.max_duration_minutes) ?? 30;
189
+ const max_file_changes = execution?.constraints?.max_file_changes ?? asFiniteNumber(constraintsConfig.max_file_changes) ?? 20;
190
+ const require_tests = asBoolean(
191
+ execution?.constraints?.require_tests ?? constraintsConfig.require_tests,
192
+ true
193
+ );
194
+ const taskContextPaths = asStringArray(executionContext.paths) ?? [];
195
+ const taskContextFiles = asStringArray(executionContext.files) ?? [];
196
+ const taskContextTasks = asStringArray(executionContext.tasks) ?? [];
197
+ const acceptance_criteria = taskAcceptance(task);
198
+ const related_task_artifacts = collectRelatedArtifacts(task, graph);
199
+ const output_requirements = [
200
+ ...DEFAULT_OUTPUT_REQUIREMENTS,
201
+ ...(task.artifacts?.produces ?? []).map((artifact) => {
202
+ if (artifact.path) {
203
+ return `Produce ${artifact.type} artifact at ${artifact.path}`;
204
+ }
205
+ if (artifact.target) {
206
+ return `Produce ${artifact.type} artifact targeting ${artifact.target}`;
207
+ }
208
+ return `Produce ${artifact.type} artifact`;
209
+ })
210
+ ];
211
+ const contract = {
212
+ task_id: task.id,
213
+ goal: computeGoal(task),
214
+ executor: execution?.agent ?? ai.default_executor,
215
+ permissions: {
216
+ read_paths,
217
+ write_paths,
218
+ allowed_commands,
219
+ forbidden_commands
220
+ },
221
+ constraints: {
222
+ max_tokens,
223
+ max_duration_minutes,
224
+ max_file_changes,
225
+ require_tests
226
+ },
227
+ context: {
228
+ paths: taskContextPaths,
229
+ files: taskContextFiles,
230
+ tasks: taskContextTasks,
231
+ acceptance_criteria,
232
+ related_task_artifacts
233
+ },
234
+ output_requirements
235
+ };
236
+ validateContract(contract);
237
+ return contract;
238
+ }
239
+
240
+ // src/routing/router.ts
241
+ function asRecord2(value) {
242
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
243
+ return {};
244
+ }
245
+ return value;
246
+ }
247
+ function readRoutingConfig(config) {
248
+ const root = asRecord2(config);
249
+ const ai = asRecord2(root.ai);
250
+ const routing = asRecord2(ai.routing);
251
+ const defaultAgent = typeof routing.default_agent === "string" && routing.default_agent.trim() || typeof ai.default_executor === "string" && ai.default_executor.trim() || "claude-sonnet";
252
+ const reviewAgent = typeof routing.review_agent === "string" && routing.review_agent.trim() || defaultAgent;
253
+ const complexAgent = typeof routing.complex_agent === "string" && routing.complex_agent.trim() || typeof routing.opus_agent === "string" && routing.opus_agent.trim() || "claude-opus";
254
+ return {
255
+ default_agent: defaultAgent,
256
+ review_agent: reviewAgent,
257
+ complex_agent: complexAgent
258
+ };
259
+ }
260
+ function select_agent(task, config) {
261
+ if (task.execution?.agent && task.execution.agent.trim().length > 0) {
262
+ return task.execution.agent.trim();
263
+ }
264
+ const routing = readRoutingConfig(config);
265
+ if (task.complexity === "large" || task.complexity === "unknown" || task.type === "spike") {
266
+ return routing.complex_agent;
267
+ }
268
+ const hasReviewStep = (task.execution?.runbook ?? []).some((step) => step.action === "review");
269
+ if (hasReviewStep) {
270
+ return routing.review_agent;
271
+ }
272
+ return routing.default_agent;
273
+ }
274
+
275
+ // src/sandbox/sandbox.ts
276
+ function normalizePath2(input) {
277
+ const collapsed = input.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/\/$/, "").trim();
278
+ return collapsed.length > 0 ? collapsed : ".";
279
+ }
280
+ function isSubPath2(candidate, parent) {
281
+ const pathValue = normalizePath2(candidate);
282
+ const parentValue = normalizePath2(parent);
283
+ if (parentValue === "." || parentValue === "") return true;
284
+ return pathValue === parentValue || pathValue.startsWith(`${parentValue}/`);
285
+ }
286
+ function normalizeCommand2(command) {
287
+ return command.trim().replace(/\s+/g, " ").toLowerCase();
288
+ }
289
+ function commandMatchesRule2(command, rule) {
290
+ const normalizedCommand = normalizeCommand2(command);
291
+ const normalizedRule = normalizeCommand2(rule);
292
+ return normalizedCommand === normalizedRule || normalizedCommand.startsWith(`${normalizedRule} `);
293
+ }
294
+ function hasBlockedShellSyntax(command) {
295
+ return /(\|\||&&|[|;]|`|\$\()/.test(command);
296
+ }
297
+ function validate_command(command, contract) {
298
+ if (!command || command.trim().length === 0) return false;
299
+ if (hasBlockedShellSyntax(command)) return false;
300
+ if (contract.permissions.forbidden_commands.some((rule) => commandMatchesRule2(command, rule))) {
301
+ return false;
302
+ }
303
+ const allowed = contract.permissions.allowed_commands;
304
+ if (allowed.length === 0) {
305
+ return true;
306
+ }
307
+ return allowed.some((rule) => commandMatchesRule2(command, rule));
308
+ }
309
+ function validate_file_access(pathValue, mode, contract) {
310
+ if (!pathValue || pathValue.trim().length === 0) return false;
311
+ const normalized = normalizePath2(pathValue);
312
+ const canRead = contract.permissions.read_paths.some((readPath) => isSubPath2(normalized, readPath));
313
+ if (!canRead) return false;
314
+ if (mode === "read") return true;
315
+ return contract.permissions.write_paths.some((writePath) => isSubPath2(normalized, writePath));
316
+ }
317
+ function constraint_violation_reasons(runState, contract) {
318
+ const reasons = [];
319
+ if (runState.tokens_used > contract.constraints.max_tokens) {
320
+ reasons.push("token budget exceeded");
321
+ }
322
+ if (runState.duration_minutes > contract.constraints.max_duration_minutes) {
323
+ reasons.push("duration limit exceeded");
324
+ }
325
+ if (runState.file_changes > contract.constraints.max_file_changes) {
326
+ reasons.push("file change limit exceeded");
327
+ }
328
+ return reasons;
329
+ }
330
+ function enforce_constraints(runState, contract) {
331
+ return constraint_violation_reasons(runState, contract).length === 0;
332
+ }
333
+
334
+ // src/logging/run-logger.ts
335
+ var import_node_fs = __toESM(require("fs"), 1);
336
+ var import_node_path = __toESM(require("path"), 1);
337
+ var import_node_crypto = __toESM(require("crypto"), 1);
338
+ var import_coop_core = require("@kitsy/coop-core");
339
+ function randomSuffix() {
340
+ return import_node_crypto.default.randomBytes(2).toString("hex").toUpperCase();
341
+ }
342
+ function compactDateToken(now) {
343
+ return now.toISOString().replace(/[-:]/g, "").slice(0, 15);
344
+ }
345
+ function generateRunId(now) {
346
+ return `RUN-${compactDateToken(now)}-${randomSuffix()}`;
347
+ }
348
+ function cloneRun(run) {
349
+ return {
350
+ ...run,
351
+ steps: run.steps.map((step) => ({ ...step })),
352
+ resources_consumed: { ...run.resources_consumed }
353
+ };
354
+ }
355
+ function create_run(task, executor, now = /* @__PURE__ */ new Date()) {
356
+ return {
357
+ id: generateRunId(now),
358
+ task: task.id,
359
+ executor,
360
+ status: "running",
361
+ started: now.toISOString(),
362
+ steps: [],
363
+ resources_consumed: {
364
+ ai_tokens: 0,
365
+ compute_minutes: 0,
366
+ file_changes: 0
367
+ }
368
+ };
369
+ }
370
+ function log_step(run, step) {
371
+ const next = cloneRun(run);
372
+ next.steps.push({ ...step });
373
+ return next;
374
+ }
375
+ function finalize_run(run, status, now = /* @__PURE__ */ new Date()) {
376
+ const next = cloneRun(run);
377
+ next.status = status;
378
+ next.completed = now.toISOString();
379
+ return next;
380
+ }
381
+ function write_run(run, coopDir) {
382
+ const runsDir = import_node_path.default.join(coopDir, "runs");
383
+ import_node_fs.default.mkdirSync(runsDir, { recursive: true });
384
+ const filePath = import_node_path.default.join(runsDir, `${run.id}.yml`);
385
+ (0, import_coop_core.writeYamlFile)(filePath, run);
386
+ return filePath;
387
+ }
388
+
389
+ // src/executor/executor.ts
390
+ var import_node_child_process = require("child_process");
391
+ var import_node_path2 = __toESM(require("path"), 1);
392
+ function defaultAgentClient() {
393
+ return {
394
+ async generate(input) {
395
+ const tokenEstimate = Math.max(1, Math.ceil(input.prompt.length / 4));
396
+ return {
397
+ summary: `Generated draft for '${input.step.step}'.`,
398
+ tokens_used: tokenEstimate,
399
+ file_changes: 0
400
+ };
401
+ },
402
+ async review(input) {
403
+ const tokenEstimate = Math.max(1, Math.ceil(input.prompt.length / 6));
404
+ return {
405
+ summary: `AI review for '${input.step.step}' completed.`,
406
+ tokens_used: tokenEstimate,
407
+ file_changes: 0
408
+ };
409
+ }
410
+ };
411
+ }
412
+ function nowIso() {
413
+ return (/* @__PURE__ */ new Date()).toISOString();
414
+ }
415
+ function durationSeconds(startMs) {
416
+ return Math.max(0, Math.round((Date.now() - startMs) / 1e3));
417
+ }
418
+ function countChangedFiles(repoRoot) {
419
+ const result = (0, import_node_child_process.spawnSync)("git", ["status", "--porcelain"], {
420
+ cwd: repoRoot,
421
+ encoding: "utf8",
422
+ windowsHide: true
423
+ });
424
+ if ((result.status ?? 1) !== 0) {
425
+ return 0;
426
+ }
427
+ return (result.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).length;
428
+ }
429
+ function executeCommand(command, repoRoot) {
430
+ const result = (0, import_node_child_process.spawnSync)(command, {
431
+ cwd: repoRoot,
432
+ shell: true,
433
+ encoding: "utf8",
434
+ windowsHide: true
435
+ });
436
+ const stdout = (result.stdout ?? "").trim();
437
+ const stderr = (result.stderr ?? "").trim();
438
+ const exitCode = result.status ?? 1;
439
+ const summary = [stdout, stderr].filter(Boolean).join("\n").trim();
440
+ return {
441
+ ok: exitCode === 0,
442
+ summary: summary || `Command exited with code ${exitCode}.`,
443
+ exitCode
444
+ };
445
+ }
446
+ function buildPrompt(task, contract, stepId, description) {
447
+ const sections = [
448
+ `Task: ${task.id}`,
449
+ `Goal: ${contract.goal}`,
450
+ `Step: ${stepId}`,
451
+ description ? `Description: ${description}` : "",
452
+ contract.context.acceptance_criteria.length > 0 ? `Acceptance: ${contract.context.acceptance_criteria.join(" | ")}` : "",
453
+ contract.context.paths.length > 0 ? `Context paths: ${contract.context.paths.join(", ")}` : "",
454
+ contract.context.files.length > 0 ? `Context files: ${contract.context.files.join(", ")}` : ""
455
+ ].filter(Boolean);
456
+ return sections.join("\n");
457
+ }
458
+ async function execute_task(task, contract, _graph, options = {}) {
459
+ const runbook = task.execution?.runbook ?? [];
460
+ if (runbook.length === 0) {
461
+ throw new Error(`Task '${task.id}' has no execution.runbook steps.`);
462
+ }
463
+ const selectedSteps = options.step ? runbook.filter((step) => step.step === options.step) : runbook;
464
+ if (selectedSteps.length === 0) {
465
+ throw new Error(`Step '${options.step}' not found in task '${task.id}'.`);
466
+ }
467
+ const repoRoot = options.repo_root ?? process.cwd();
468
+ const coopDir = options.coop_dir ?? import_node_path2.default.join(repoRoot, ".coop");
469
+ const onProgress = options.on_progress ?? (() => {
470
+ });
471
+ const agent = options.agent_client ?? defaultAgentClient();
472
+ const runState = {
473
+ tokens_used: 0,
474
+ duration_minutes: 0,
475
+ file_changes: countChangedFiles(repoRoot)
476
+ };
477
+ let run = create_run(task, contract.executor);
478
+ let finalStatus = "running";
479
+ for (const step of selectedSteps) {
480
+ const started = nowIso();
481
+ const startMs = Date.now();
482
+ onProgress(`Running step '${step.step}' (${step.action})...`);
483
+ let stepStatus = "completed";
484
+ let outputSummary = "";
485
+ let notes;
486
+ let exitCode;
487
+ let stepTokens = 0;
488
+ if (step.action === "run" || step.action === "test") {
489
+ if (!step.command) {
490
+ throw new Error(`Step '${step.step}' requires a command for action '${step.action}'.`);
491
+ }
492
+ if (!validate_command(step.command, contract)) {
493
+ stepStatus = "failed";
494
+ outputSummary = `Command rejected by sandbox: ${step.command}`;
495
+ } else {
496
+ const command = executeCommand(step.command, repoRoot);
497
+ stepStatus = command.ok ? "completed" : "failed";
498
+ outputSummary = command.summary;
499
+ exitCode = command.exitCode;
500
+ }
501
+ } else if (step.action === "generate") {
502
+ const prompt = buildPrompt(task, contract, step.step, step.description ?? "");
503
+ const response = await agent.generate({ task, step, contract, prompt });
504
+ stepTokens = response.tokens_used ?? 0;
505
+ runState.tokens_used += stepTokens;
506
+ outputSummary = response.summary;
507
+ runState.file_changes += response.file_changes ?? 0;
508
+ } else if (step.action === "review") {
509
+ if ((step.reviewer ?? "human") === "human") {
510
+ stepStatus = "paused";
511
+ notes = "Waiting for human reviewer.";
512
+ outputSummary = notes;
513
+ } else {
514
+ const prompt = buildPrompt(task, contract, step.step, step.description ?? "Perform review");
515
+ const response = await agent.review({ task, step, contract, prompt });
516
+ stepTokens = response.tokens_used ?? 0;
517
+ runState.tokens_used += stepTokens;
518
+ outputSummary = response.summary;
519
+ }
520
+ } else {
521
+ stepStatus = "failed";
522
+ outputSummary = `Unsupported action '${step.action}'.`;
523
+ }
524
+ const duration = durationSeconds(startMs);
525
+ runState.duration_minutes += duration / 60;
526
+ runState.file_changes = Math.max(runState.file_changes, countChangedFiles(repoRoot));
527
+ const stepResult = {
528
+ step: step.step,
529
+ action: step.action,
530
+ status: stepStatus,
531
+ started,
532
+ completed: nowIso(),
533
+ duration_seconds: duration,
534
+ output_summary: outputSummary,
535
+ reviewer: step.reviewer,
536
+ notes,
537
+ exit_code: exitCode,
538
+ tokens_used: stepTokens || void 0
539
+ };
540
+ run = log_step(run, stepResult);
541
+ run.resources_consumed.ai_tokens = runState.tokens_used;
542
+ run.resources_consumed.compute_minutes = Number(runState.duration_minutes.toFixed(2));
543
+ run.resources_consumed.file_changes = runState.file_changes;
544
+ if (!enforce_constraints(runState, contract)) {
545
+ finalStatus = "paused";
546
+ const reasons = constraint_violation_reasons(runState, contract);
547
+ run = log_step(run, {
548
+ step: `${step.step}-constraint-check`,
549
+ action: "constraint",
550
+ status: "paused",
551
+ started: nowIso(),
552
+ completed: nowIso(),
553
+ duration_seconds: 0,
554
+ notes: reasons.join("; "),
555
+ output_summary: reasons.join("; ")
556
+ });
557
+ break;
558
+ }
559
+ if (stepStatus === "failed") {
560
+ finalStatus = "failed";
561
+ break;
562
+ }
563
+ if (stepStatus === "paused") {
564
+ finalStatus = "paused";
565
+ break;
566
+ }
567
+ }
568
+ if (finalStatus === "running") {
569
+ finalStatus = "completed";
570
+ }
571
+ run = finalize_run(run, finalStatus);
572
+ const logPath = write_run(run, coopDir);
573
+ return {
574
+ status: finalStatus,
575
+ run,
576
+ log_path: logPath
577
+ };
578
+ }
579
+
580
+ // src/decomposition/decompose.ts
581
+ function nonEmptyLines(input) {
582
+ return input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
583
+ }
584
+ function extractBulletIdeas(body) {
585
+ const lines = nonEmptyLines(body);
586
+ return lines.filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter(Boolean).slice(0, 5);
587
+ }
588
+ function sentenceCase(value) {
589
+ if (value.length === 0) return value;
590
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
591
+ }
592
+ function defaultDecomposition(input) {
593
+ const bullets = extractBulletIdeas(input.body);
594
+ if (bullets.length > 0) {
595
+ return bullets.slice(0, 3).map((bullet, index) => ({
596
+ title: sentenceCase(bullet),
597
+ body: `Derived from idea ${input.idea_id}: ${input.title}`,
598
+ type: index === 0 ? "spike" : "feature",
599
+ priority: index === 0 ? "p1" : "p2",
600
+ track: "unassigned"
601
+ }));
602
+ }
603
+ return [
604
+ {
605
+ title: `Define scope and acceptance for ${input.title}`,
606
+ body: `Derived from idea ${input.idea_id}. Capture constraints and success criteria.`,
607
+ type: "spike",
608
+ priority: "p1",
609
+ track: "unassigned"
610
+ },
611
+ {
612
+ title: `Implement ${input.title}`,
613
+ body: `Derived from idea ${input.idea_id}. Build the primary functionality.`,
614
+ type: "feature",
615
+ priority: "p1",
616
+ track: "unassigned"
617
+ },
618
+ {
619
+ title: `Validate and rollout ${input.title}`,
620
+ body: `Derived from idea ${input.idea_id}. Add tests and release checklist.`,
621
+ type: "chore",
622
+ priority: "p2",
623
+ track: "unassigned"
624
+ }
625
+ ];
626
+ }
627
+ function build_decomposition_prompt(input) {
628
+ return [
629
+ "You are a planning agent for COOP.",
630
+ "Decompose the idea into 2-5 implementation tasks.",
631
+ "Each task needs: title, type(feature|bug|chore|spike), priority(p0-p3), and body.",
632
+ `Idea ID: ${input.idea_id}`,
633
+ `Title: ${input.title}`,
634
+ "Body:",
635
+ input.body || "(empty)"
636
+ ].join("\n");
637
+ }
638
+ async function decompose_idea_to_tasks(input, client) {
639
+ const prompt = build_decomposition_prompt(input);
640
+ if (!client) {
641
+ return defaultDecomposition(input);
642
+ }
643
+ const drafts = await client.decompose(prompt, input);
644
+ if (!Array.isArray(drafts) || drafts.length === 0) {
645
+ return defaultDecomposition(input);
646
+ }
647
+ return drafts;
648
+ }
649
+
650
+ // src/providers/config.ts
651
+ var DEFAULT_MODELS = {
652
+ openai: "gpt-5-mini",
653
+ anthropic: "claude-3-5-sonnet-latest",
654
+ gemini: "gemini-2.0-flash",
655
+ ollama: "llama3.2"
656
+ };
657
+ var DEFAULT_KEY_ENV = {
658
+ openai: "OPENAI_API_KEY",
659
+ anthropic: "ANTHROPIC_API_KEY",
660
+ gemini: "GEMINI_API_KEY"
29
661
  };
662
+ var DEFAULT_BASE_URL = {
663
+ openai: "https://api.openai.com/v1",
664
+ anthropic: "https://api.anthropic.com/v1",
665
+ gemini: "https://generativelanguage.googleapis.com/v1beta",
666
+ ollama: "http://localhost:11434"
667
+ };
668
+ function asRecord3(value) {
669
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
670
+ return {};
671
+ }
672
+ return value;
673
+ }
674
+ function asString(value) {
675
+ if (typeof value !== "string") return null;
676
+ const trimmed = value.trim();
677
+ return trimmed.length > 0 ? trimmed : null;
678
+ }
679
+ function asFinite(value) {
680
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
681
+ return value;
682
+ }
683
+ function readProvider(value) {
684
+ const normalized = asString(value)?.toLowerCase();
685
+ if (normalized === "openai" || normalized === "anthropic" || normalized === "gemini" || normalized === "ollama" || normalized === "mock") {
686
+ return normalized;
687
+ }
688
+ return "mock";
689
+ }
690
+ function lookupProviderSection(ai, provider) {
691
+ return asRecord3(ai[provider]);
692
+ }
693
+ function resolve_provider_config(config) {
694
+ const root = asRecord3(config);
695
+ const ai = asRecord3(root.ai);
696
+ const provider = readProvider(ai.provider);
697
+ if (provider === "mock") {
698
+ return {
699
+ provider,
700
+ model: "mock-local",
701
+ temperature: 0.2,
702
+ max_output_tokens: 1024,
703
+ timeout_ms: 6e4
704
+ };
705
+ }
706
+ const section = lookupProviderSection(ai, provider);
707
+ const model = asString(section.model) ?? asString(ai.model) ?? DEFAULT_MODELS[provider];
708
+ const base_url = asString(section.base_url) ?? asString(ai.base_url) ?? DEFAULT_BASE_URL[provider];
709
+ const temperature = asFinite(section.temperature) ?? asFinite(ai.temperature) ?? 0.2;
710
+ const max_output_tokens = asFinite(section.max_output_tokens) ?? asFinite(ai.max_output_tokens) ?? 1024;
711
+ const timeout_ms = asFinite(section.timeout_ms) ?? asFinite(ai.timeout_ms) ?? 6e4;
712
+ let api_key;
713
+ if (provider !== "ollama") {
714
+ const envName = asString(section.api_key_env) ?? asString(ai.api_key_env) ?? DEFAULT_KEY_ENV[provider];
715
+ const envValue = envName ? asString(process.env[envName]) : null;
716
+ api_key = asString(section.api_key) ?? asString(ai.api_key) ?? envValue ?? void 0;
717
+ if (!api_key) {
718
+ throw new Error(
719
+ `Missing API key for provider '${provider}'. Set ${envName ?? "the configured api_key_env"} or ai.${provider}.api_key.`
720
+ );
721
+ }
722
+ }
723
+ return {
724
+ provider,
725
+ model,
726
+ base_url,
727
+ api_key,
728
+ temperature,
729
+ max_output_tokens,
730
+ timeout_ms
731
+ };
732
+ }
733
+
734
+ // src/providers/http.ts
735
+ async function post_json(url, init) {
736
+ const controller = new AbortController();
737
+ const timeout = setTimeout(() => controller.abort(), Math.max(1, init.timeout_ms));
738
+ try {
739
+ const response = await fetch(url, {
740
+ method: "POST",
741
+ headers: {
742
+ "content-type": "application/json",
743
+ ...init.headers ?? {}
744
+ },
745
+ body: JSON.stringify(init.body),
746
+ signal: controller.signal
747
+ });
748
+ const text = await response.text();
749
+ if (!response.ok) {
750
+ throw new Error(`Provider HTTP ${response.status}: ${text.slice(0, 500)}`);
751
+ }
752
+ try {
753
+ return JSON.parse(text);
754
+ } catch (error) {
755
+ const message = error instanceof Error ? error.message : String(error);
756
+ throw new Error(`Provider returned invalid JSON: ${message}`);
757
+ }
758
+ } finally {
759
+ clearTimeout(timeout);
760
+ }
761
+ }
762
+
763
+ // src/providers/anthropic.ts
764
+ var AnthropicProvider = class {
765
+ constructor(config) {
766
+ this.config = config;
767
+ }
768
+ name = "anthropic";
769
+ async complete(input) {
770
+ const base = this.config.base_url ?? "https://api.anthropic.com/v1";
771
+ const json = await post_json(`${base}/messages`, {
772
+ headers: {
773
+ "x-api-key": this.config.api_key ?? "",
774
+ "anthropic-version": "2023-06-01"
775
+ },
776
+ body: {
777
+ model: this.config.model,
778
+ max_tokens: this.config.max_output_tokens,
779
+ temperature: this.config.temperature,
780
+ system: input.system,
781
+ messages: [{ role: "user", content: input.prompt }]
782
+ },
783
+ timeout_ms: this.config.timeout_ms
784
+ });
785
+ const text = (json.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text?.trim() ?? "").filter(Boolean).join("\n").trim();
786
+ if (!text) {
787
+ throw new Error("Anthropic response did not include completion text.");
788
+ }
789
+ const tokens = (json.usage?.input_tokens ?? 0) + (json.usage?.output_tokens ?? 0);
790
+ return {
791
+ text,
792
+ total_tokens: tokens > 0 ? tokens : void 0
793
+ };
794
+ }
795
+ };
796
+
797
+ // src/providers/gemini.ts
798
+ var GeminiProvider = class {
799
+ constructor(config) {
800
+ this.config = config;
801
+ }
802
+ name = "gemini";
803
+ async complete(input) {
804
+ const base = this.config.base_url ?? "https://generativelanguage.googleapis.com/v1beta";
805
+ const key = this.config.api_key ?? "";
806
+ const model = encodeURIComponent(this.config.model);
807
+ const json = await post_json(`${base}/models/${model}:generateContent?key=${key}`, {
808
+ body: {
809
+ systemInstruction: {
810
+ parts: [{ text: input.system }]
811
+ },
812
+ generationConfig: {
813
+ temperature: this.config.temperature,
814
+ maxOutputTokens: this.config.max_output_tokens
815
+ },
816
+ contents: [
817
+ {
818
+ role: "user",
819
+ parts: [{ text: input.prompt }]
820
+ }
821
+ ]
822
+ },
823
+ timeout_ms: this.config.timeout_ms
824
+ });
825
+ const text = (json.candidates?.[0]?.content?.parts ?? []).map((part) => part.text?.trim() ?? "").filter(Boolean).join("\n").trim();
826
+ if (!text) {
827
+ throw new Error("Gemini response did not include completion text.");
828
+ }
829
+ return {
830
+ text,
831
+ total_tokens: json.usageMetadata?.totalTokenCount
832
+ };
833
+ }
834
+ };
835
+
836
+ // src/providers/mock.ts
837
+ var MockProvider = class {
838
+ name = "mock";
839
+ async complete(input) {
840
+ const preview = input.prompt.trim().split(/\r?\n/).slice(0, 3).join(" ").slice(0, 120);
841
+ const text = `MOCK_RESPONSE: ${preview}`;
842
+ return {
843
+ text,
844
+ total_tokens: Math.max(1, Math.ceil((input.system.length + input.prompt.length) / 4))
845
+ };
846
+ }
847
+ };
848
+
849
+ // src/providers/ollama.ts
850
+ var OllamaProvider = class {
851
+ constructor(config) {
852
+ this.config = config;
853
+ }
854
+ name = "ollama";
855
+ async complete(input) {
856
+ const base = this.config.base_url ?? "http://localhost:11434";
857
+ const json = await post_json(`${base}/api/chat`, {
858
+ body: {
859
+ model: this.config.model,
860
+ stream: false,
861
+ options: {
862
+ temperature: this.config.temperature,
863
+ num_predict: this.config.max_output_tokens
864
+ },
865
+ messages: [
866
+ { role: "system", content: input.system },
867
+ { role: "user", content: input.prompt }
868
+ ]
869
+ },
870
+ timeout_ms: this.config.timeout_ms
871
+ });
872
+ const text = json.message?.content?.trim();
873
+ if (!text) {
874
+ throw new Error("Ollama response did not include completion text.");
875
+ }
876
+ const tokens = (json.prompt_eval_count ?? 0) + (json.eval_count ?? 0);
877
+ return {
878
+ text,
879
+ total_tokens: tokens > 0 ? tokens : void 0
880
+ };
881
+ }
882
+ };
883
+
884
+ // src/providers/openai.ts
885
+ var OpenAiProvider = class {
886
+ constructor(config) {
887
+ this.config = config;
888
+ }
889
+ name = "openai";
890
+ async complete(input) {
891
+ const base = this.config.base_url ?? "https://api.openai.com/v1";
892
+ const json = await post_json(`${base}/chat/completions`, {
893
+ headers: {
894
+ authorization: `Bearer ${this.config.api_key ?? ""}`
895
+ },
896
+ body: {
897
+ model: this.config.model,
898
+ temperature: this.config.temperature,
899
+ max_tokens: this.config.max_output_tokens,
900
+ messages: [
901
+ { role: "system", content: input.system },
902
+ { role: "user", content: input.prompt }
903
+ ]
904
+ },
905
+ timeout_ms: this.config.timeout_ms
906
+ });
907
+ const text = json.choices?.[0]?.message?.content?.trim();
908
+ if (!text) {
909
+ throw new Error("OpenAI response did not include completion text.");
910
+ }
911
+ return {
912
+ text,
913
+ total_tokens: json.usage?.total_tokens
914
+ };
915
+ }
916
+ };
917
+
918
+ // src/providers/factory.ts
919
+ function create_provider(config) {
920
+ const resolved = resolve_provider_config(config);
921
+ switch (resolved.provider) {
922
+ case "openai":
923
+ return new OpenAiProvider(resolved);
924
+ case "anthropic":
925
+ return new AnthropicProvider(resolved);
926
+ case "gemini":
927
+ return new GeminiProvider(resolved);
928
+ case "ollama":
929
+ return new OllamaProvider(resolved);
930
+ case "mock":
931
+ default:
932
+ return new MockProvider();
933
+ }
934
+ }
935
+
936
+ // src/providers/provider-client.ts
937
+ function parseJsonArray(text) {
938
+ const trimmed = text.trim();
939
+ try {
940
+ const direct = JSON.parse(trimmed);
941
+ if (Array.isArray(direct)) return direct;
942
+ } catch {
943
+ }
944
+ const fence = trimmed.match(/```json\s*([\s\S]*?)```/i);
945
+ if (fence?.[1]) {
946
+ try {
947
+ const parsed = JSON.parse(fence[1]);
948
+ if (Array.isArray(parsed)) return parsed;
949
+ } catch {
950
+ }
951
+ }
952
+ const start = trimmed.indexOf("[");
953
+ const end = trimmed.lastIndexOf("]");
954
+ if (start >= 0 && end > start) {
955
+ const candidate = trimmed.slice(start, end + 1);
956
+ try {
957
+ const parsed = JSON.parse(candidate);
958
+ if (Array.isArray(parsed)) return parsed;
959
+ } catch {
960
+ }
961
+ }
962
+ return null;
963
+ }
964
+ function toTaskDrafts(value) {
965
+ const out = [];
966
+ for (const entry of value) {
967
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
968
+ const record = entry;
969
+ const title = typeof record.title === "string" ? record.title.trim() : "";
970
+ if (!title) continue;
971
+ out.push({
972
+ title,
973
+ body: typeof record.body === "string" ? record.body : void 0,
974
+ type: record.type === "feature" || record.type === "bug" || record.type === "chore" || record.type === "spike" ? record.type : void 0,
975
+ priority: record.priority === "p0" || record.priority === "p1" || record.priority === "p2" || record.priority === "p3" ? record.priority : void 0,
976
+ track: typeof record.track === "string" ? record.track : void 0
977
+ });
978
+ }
979
+ return out;
980
+ }
981
+ function asAgentResponse(text, tokens) {
982
+ return {
983
+ summary: text.trim(),
984
+ tokens_used: tokens
985
+ };
986
+ }
987
+ function create_provider_agent_client(config) {
988
+ const provider = create_provider(config);
989
+ return {
990
+ async generate(input) {
991
+ const result = await provider.complete({
992
+ system: "You are an implementation agent. Return a concise execution summary for the requested step.",
993
+ prompt: input.prompt
994
+ });
995
+ return asAgentResponse(result.text, result.total_tokens);
996
+ },
997
+ async review(input) {
998
+ const result = await provider.complete({
999
+ system: "You are a code-review agent. Return concise review findings, risks, and verdict.",
1000
+ prompt: input.prompt
1001
+ });
1002
+ return asAgentResponse(result.text, result.total_tokens);
1003
+ }
1004
+ };
1005
+ }
1006
+ function create_provider_idea_decomposer(config) {
1007
+ const provider = create_provider(config);
1008
+ return {
1009
+ async decompose(prompt) {
1010
+ const result = await provider.complete({
1011
+ system: "Return ONLY JSON array. Each item must include title, type, priority, body, track.",
1012
+ prompt
1013
+ });
1014
+ const parsed = parseJsonArray(result.text);
1015
+ if (!parsed) {
1016
+ return [];
1017
+ }
1018
+ return toTaskDrafts(parsed);
1019
+ }
1020
+ };
1021
+ }
30
1022
  // Annotate the CommonJS export names for ESM import in node:
31
1023
  0 && (module.exports = {
32
- coopAiRuntime
1024
+ build_contract,
1025
+ build_decomposition_prompt,
1026
+ constraint_violation_reasons,
1027
+ create_provider,
1028
+ create_provider_agent_client,
1029
+ create_provider_idea_decomposer,
1030
+ create_run,
1031
+ decompose_idea_to_tasks,
1032
+ enforce_constraints,
1033
+ execute_task,
1034
+ finalize_run,
1035
+ log_step,
1036
+ resolve_provider_config,
1037
+ select_agent,
1038
+ validate_command,
1039
+ validate_file_access,
1040
+ write_run
33
1041
  });