@kitsy/coop-ai 0.0.1 → 2.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 +1013 -5
- package/dist/index.d.cts +204 -6
- package/dist/index.d.ts +204 -6
- package/dist/index.js +985 -5
- package/package.json +12 -6
- package/src/index.ts +0 -9
- package/tsconfig.json +0 -7
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
});
|