@kitman-sh/cli 0.0.0-stage → 0.6.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/chunk-3j7sewn0.js +4618 -0
- package/dist/chunk-6h8ghah0.js +14 -0
- package/dist/chunk-brp7cekg.js +13 -0
- package/dist/chunk-dsefng1v.js +1085 -0
- package/dist/chunk-fc0v325q.js +75 -0
- package/dist/chunk-h8ctzn91.js +241 -0
- package/dist/chunk-hrvcz0m4.js +211 -0
- package/dist/chunk-hw7qkap1.js +1517 -0
- package/dist/chunk-n32jhswz.js +964 -0
- package/dist/chunk-tfr74ays.js +7060 -0
- package/dist/chunk-txyq8r41.js +25537 -0
- package/dist/chunk-xdk488qp.js +364 -0
- package/dist/egress-proxy.ts +107 -0
- package/dist/index.js +5738 -0
- package/package.json +16 -4
- package/README.md +0 -3
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
isString,
|
|
4
|
+
Service2,
|
|
5
|
+
fromInputUnsafe,
|
|
6
|
+
isZero,
|
|
7
|
+
seconds,
|
|
8
|
+
isLessThanOrEqualTo,
|
|
9
|
+
forEach,
|
|
10
|
+
succeed5,
|
|
11
|
+
gen,
|
|
12
|
+
map3,
|
|
13
|
+
as,
|
|
14
|
+
catchTag,
|
|
15
|
+
tapError,
|
|
16
|
+
ignore,
|
|
17
|
+
sleep,
|
|
18
|
+
fnUntraced,
|
|
19
|
+
logInfo,
|
|
20
|
+
optional,
|
|
21
|
+
Literal,
|
|
22
|
+
String2,
|
|
23
|
+
Boolean2,
|
|
24
|
+
Struct,
|
|
25
|
+
Record,
|
|
26
|
+
ArraySchema,
|
|
27
|
+
NonEmptyArray,
|
|
28
|
+
Union,
|
|
29
|
+
Literals,
|
|
30
|
+
Finite,
|
|
31
|
+
Int,
|
|
32
|
+
TaggedError2,
|
|
33
|
+
Json
|
|
34
|
+
} from "./chunk-txyq8r41.js";
|
|
35
|
+
import"./chunk-hrvcz0m4.js";
|
|
36
|
+
import {
|
|
37
|
+
LoopOutcome,
|
|
38
|
+
defineLoop,
|
|
39
|
+
make2,
|
|
40
|
+
await_,
|
|
41
|
+
make3,
|
|
42
|
+
evidenceGate,
|
|
43
|
+
ChildFailed,
|
|
44
|
+
agentWork,
|
|
45
|
+
ask,
|
|
46
|
+
call,
|
|
47
|
+
waitFor,
|
|
48
|
+
verify,
|
|
49
|
+
PlanTickets,
|
|
50
|
+
planWaves,
|
|
51
|
+
ChangeRequestResult,
|
|
52
|
+
CreateTasksResult,
|
|
53
|
+
changeRequest,
|
|
54
|
+
writeBack,
|
|
55
|
+
artifact,
|
|
56
|
+
note,
|
|
57
|
+
comment,
|
|
58
|
+
createTasks,
|
|
59
|
+
integrationBranch,
|
|
60
|
+
mergeChangeRequest,
|
|
61
|
+
release2,
|
|
62
|
+
Finding,
|
|
63
|
+
MergedFinding,
|
|
64
|
+
Rebuttal,
|
|
65
|
+
RebuttalVerdict,
|
|
66
|
+
ScopeJudgement,
|
|
67
|
+
isBlocking,
|
|
68
|
+
mergeFindings,
|
|
69
|
+
settleRound,
|
|
70
|
+
rallyStop
|
|
71
|
+
} from "./chunk-n32jhswz.js";
|
|
72
|
+
|
|
73
|
+
// node_modules/.bun/effect@4.0.4/node_modules/effect/dist/workflow/DurableClock.js
|
|
74
|
+
var TypeId = "~effect/workflow/DurableClock";
|
|
75
|
+
var make = (options) => ({
|
|
76
|
+
[TypeId]: TypeId,
|
|
77
|
+
name: options.name,
|
|
78
|
+
duration: fromInputUnsafe(options.duration),
|
|
79
|
+
deferred: make2(`DurableClock/${options.name}`)
|
|
80
|
+
});
|
|
81
|
+
var EngineTag = /* @__PURE__ */ Service2("effect/workflow/WorkflowEngine");
|
|
82
|
+
var InstanceTag = /* @__PURE__ */ Service2("effect/workflow/WorkflowEngine/WorkflowInstance");
|
|
83
|
+
var sleep2 = /* @__PURE__ */ fnUntraced(function* (options) {
|
|
84
|
+
const duration = fromInputUnsafe(options.duration);
|
|
85
|
+
if (isZero(duration)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const inMemoryThreshold = options.inMemoryThreshold !== undefined ? fromInputUnsafe(options.inMemoryThreshold) : defaultInMemoryThreshold;
|
|
89
|
+
if (isLessThanOrEqualTo(duration, inMemoryThreshold)) {
|
|
90
|
+
return yield* make3({
|
|
91
|
+
name: `DurableClock/${options.name}`,
|
|
92
|
+
execute: sleep(duration)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const engine = yield* EngineTag;
|
|
96
|
+
const instance = yield* InstanceTag;
|
|
97
|
+
const clock = make(options);
|
|
98
|
+
yield* engine.scheduleClock(instance.workflow, {
|
|
99
|
+
executionId: instance.executionId,
|
|
100
|
+
clock
|
|
101
|
+
});
|
|
102
|
+
return yield* await_(clock.deferred);
|
|
103
|
+
});
|
|
104
|
+
var defaultInMemoryThreshold = /* @__PURE__ */ seconds(60);
|
|
105
|
+
|
|
106
|
+
// packages/loops/src/change-request-body.ts
|
|
107
|
+
var unsettledItems = (settled, decision) => decision.unsettled.map((id) => {
|
|
108
|
+
const standing = settled.standing.find((entry) => entry.finding.id === id);
|
|
109
|
+
if (standing !== undefined)
|
|
110
|
+
return `Dispute stands on ${id}: ${standing.finding.detail} (rebutted: ${standing.rebuttal.reason})`;
|
|
111
|
+
return `Unresolved ${settled.open.find((finding) => finding.id === id)?.severity ?? "blocker"} at ${id}: ${settled.open.find((finding) => finding.id === id)?.detail ?? ""}`;
|
|
112
|
+
});
|
|
113
|
+
var evidenceTable = (head) => [
|
|
114
|
+
`Collected by a Runner for revision \`${head.revision}\`, tree \`${head.tree}\`; it holds for any revision with this tree.`,
|
|
115
|
+
"",
|
|
116
|
+
"| Check | Command | Result |",
|
|
117
|
+
"| --- | --- | --- |",
|
|
118
|
+
...head.checks.map((entry) => `| ${entry.name} | \`${entry.command}\` | ${entry.status}${entry.preExisting ? " (also fails on the default branch)" : ""} |`)
|
|
119
|
+
].join(`
|
|
120
|
+
`);
|
|
121
|
+
var changeRequestBody = (build) => [
|
|
122
|
+
build.summary,
|
|
123
|
+
"## Evidence",
|
|
124
|
+
evidenceTable(build.head),
|
|
125
|
+
"## Claims",
|
|
126
|
+
"What the agent says and Kitman has not checked.",
|
|
127
|
+
...build.claims.length === 0 ? ["None."] : build.claims.map((claim) => `- ${claim}`),
|
|
128
|
+
"## Unsettled",
|
|
129
|
+
...build.unsettled.length === 0 ? ["Nothing is unsettled."] : build.unsettled.map((item) => `- ${item}`),
|
|
130
|
+
"## Review",
|
|
131
|
+
`${build.rounds} review round${build.rounds === 1 ? "" : "s"}; the rally stopped: ${build.stopped.replaceAll("_", " ")}.`
|
|
132
|
+
].join(`
|
|
133
|
+
|
|
134
|
+
`);
|
|
135
|
+
|
|
136
|
+
// packages/loops/src/review.ts
|
|
137
|
+
var seats = {
|
|
138
|
+
correctness: "Correctness: bugs, broken edge cases, data loss, security, and behaviour the change does not deliver.",
|
|
139
|
+
standards: "Standards: the repository's documented conventions, tests at the right seams, names, and needless code."
|
|
140
|
+
};
|
|
141
|
+
var Input = Struct({
|
|
142
|
+
changeRequestId: optional(String2),
|
|
143
|
+
base: optional(String2),
|
|
144
|
+
head: optional(String2),
|
|
145
|
+
rebuttals: optional(ArraySchema(Rebuttal)),
|
|
146
|
+
outcome: optional(LoopOutcome),
|
|
147
|
+
claims: optional(ArraySchema(String2))
|
|
148
|
+
});
|
|
149
|
+
var SeatOutput = Struct({
|
|
150
|
+
findings: ArraySchema(Finding),
|
|
151
|
+
verdicts: optional(ArraySchema(RebuttalVerdict)),
|
|
152
|
+
scope: optional(ScopeJudgement)
|
|
153
|
+
});
|
|
154
|
+
var Output = Struct({
|
|
155
|
+
findings: ArraySchema(MergedFinding),
|
|
156
|
+
verdicts: ArraySchema(RebuttalVerdict),
|
|
157
|
+
blocking: Boolean2,
|
|
158
|
+
artifactId: String2,
|
|
159
|
+
scope: optional(ScopeJudgement)
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
class ReviewTargetMissing extends TaggedError2()("ReviewTargetMissing", {
|
|
163
|
+
message: String2
|
|
164
|
+
}) {
|
|
165
|
+
}
|
|
166
|
+
var targetOf = (input) => {
|
|
167
|
+
if (input.changeRequestId !== undefined)
|
|
168
|
+
return `ChangeRequest ${input.changeRequestId}. Read it with \`kitman.changeRequests.get("${input.changeRequestId}")\` through Kitman's MCP, fetch its source branch, and review its diff against its target branch.`;
|
|
169
|
+
if (input.base !== undefined && input.head !== undefined)
|
|
170
|
+
return `the change \`git diff ${input.base}...${input.head}\` in your checkout.`;
|
|
171
|
+
return;
|
|
172
|
+
};
|
|
173
|
+
var scopePrompt = (input) => [
|
|
174
|
+
"If your brief has a Task, judge its scope and give it as `scope`: does the change do what the Task's description and each of its Acceptance Criteria ask? Set `met` to true only if it does all of it, and list in `unmet` one line for each criterion or part of the description that is not done. If your brief has no Task, leave `scope` out.",
|
|
175
|
+
...input.outcome === undefined ? [] : [`The Outcome to prove is ${input.outcome}.`],
|
|
176
|
+
...input.claims === undefined || input.claims.length === 0 ? [] : [
|
|
177
|
+
"The implementer claims it built these. Check them against the code rather than taking them on trust:",
|
|
178
|
+
...input.claims.map((claim) => `- ${claim}`)
|
|
179
|
+
]
|
|
180
|
+
].join(`
|
|
181
|
+
|
|
182
|
+
`);
|
|
183
|
+
var promptFor = (seat, focus, target, input) => [
|
|
184
|
+
`You are the ${seat} seat on a review panel. ${focus}`,
|
|
185
|
+
`Review ${target}`,
|
|
186
|
+
"Report each problem as a finding: severity blocker (must not ship), major (fix before merge) or minor (optional); the file path from the repository root; the line; and the detail. Report nothing you have not checked in the code.",
|
|
187
|
+
...seat === "correctness" ? [scopePrompt(input)] : [],
|
|
188
|
+
...input.rebuttals === undefined || input.rebuttals.length === 0 ? [] : [
|
|
189
|
+
"The implementer disputes these findings. For each, give a verdict: accepted if the rebuttal is right and the finding should be dropped.",
|
|
190
|
+
...input.rebuttals.map((rebuttal) => `- ${rebuttal.findingId}: ${rebuttal.reason}`)
|
|
191
|
+
]
|
|
192
|
+
].join(`
|
|
193
|
+
|
|
194
|
+
`);
|
|
195
|
+
var summaryOf = (findings) => findings.length === 0 ? "Kitman review: no findings." : [
|
|
196
|
+
`Kitman review: ${findings.length} finding${findings.length === 1 ? "" : "s"}.`,
|
|
197
|
+
...findings.map((finding) => `- **${finding.severity}** \`${finding.id}\` ${finding.detail} _(${finding.seats.join(", ")})_`)
|
|
198
|
+
].join(`
|
|
199
|
+
`);
|
|
200
|
+
var review_default = defineLoop({
|
|
201
|
+
id: "review",
|
|
202
|
+
title: "Review",
|
|
203
|
+
input: Input,
|
|
204
|
+
output: Output,
|
|
205
|
+
agents: { correctness: "reviewer", standards: "reviewer" },
|
|
206
|
+
checkout: "read",
|
|
207
|
+
capabilities: ["agent_work", "post_comment"],
|
|
208
|
+
run: (input) => gen(function* () {
|
|
209
|
+
const target = targetOf(input);
|
|
210
|
+
if (target === undefined)
|
|
211
|
+
return yield* new ReviewTargetMissing({ message: "Name a ChangeRequest, or both base and head revisions" });
|
|
212
|
+
const outputs = yield* forEach(Object.entries(seats), ([seat, focus]) => agentWork(`review:${seat}`, {
|
|
213
|
+
agent: seat,
|
|
214
|
+
prompt: promptFor(seat, focus, target, input),
|
|
215
|
+
output: SeatOutput
|
|
216
|
+
}).pipe(map3((output) => [seat, output])), { concurrency: "unbounded" });
|
|
217
|
+
const scope = outputs.find(([seat]) => seat === "correctness")?.[1].scope;
|
|
218
|
+
const findings = mergeFindings(Object.fromEntries(outputs.map(([seat, output]) => [seat, output.findings])));
|
|
219
|
+
const summary = summaryOf(findings);
|
|
220
|
+
const stored = yield* artifact("findings", {
|
|
221
|
+
kind: "review-findings",
|
|
222
|
+
title: "Review findings",
|
|
223
|
+
text: summary,
|
|
224
|
+
data: { findings }
|
|
225
|
+
});
|
|
226
|
+
if (input.changeRequestId !== undefined)
|
|
227
|
+
yield* comment("post", { changeRequestId: input.changeRequestId, body: summary });
|
|
228
|
+
return {
|
|
229
|
+
findings,
|
|
230
|
+
verdicts: outputs.flatMap(([, output]) => output.verdicts ?? []),
|
|
231
|
+
blocking: findings.some(isBlocking),
|
|
232
|
+
artifactId: stored.artifactId,
|
|
233
|
+
...scope === undefined ? undefined : { scope }
|
|
234
|
+
};
|
|
235
|
+
})
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// packages/loops/src/build.ts
|
|
239
|
+
var Input2 = Struct({
|
|
240
|
+
words: optional(String2),
|
|
241
|
+
taskId: optional(String2),
|
|
242
|
+
repoBindingId: optional(String2),
|
|
243
|
+
branch: optional(String2),
|
|
244
|
+
revision: optional(String2),
|
|
245
|
+
outcome: optional(LoopOutcome),
|
|
246
|
+
maxRounds: optional(Int)
|
|
247
|
+
});
|
|
248
|
+
var Implemented = Struct({
|
|
249
|
+
title: String2,
|
|
250
|
+
summary: String2,
|
|
251
|
+
claims: ArraySchema(String2),
|
|
252
|
+
documented: optional(ArraySchema(String2)),
|
|
253
|
+
rollback: optional(String2)
|
|
254
|
+
});
|
|
255
|
+
var Reproduced = Struct({
|
|
256
|
+
check: String2,
|
|
257
|
+
summary: String2
|
|
258
|
+
});
|
|
259
|
+
var Fixed = Struct({
|
|
260
|
+
summary: String2,
|
|
261
|
+
claims: ArraySchema(String2),
|
|
262
|
+
rebuttals: ArraySchema(Rebuttal)
|
|
263
|
+
});
|
|
264
|
+
var Output2 = Struct({
|
|
265
|
+
changeRequest: ChangeRequestResult,
|
|
266
|
+
revision: String2,
|
|
267
|
+
tree: String2,
|
|
268
|
+
settled: Boolean2,
|
|
269
|
+
unsettled: ArraySchema(String2),
|
|
270
|
+
rounds: Int,
|
|
271
|
+
stopped: Literals(["settled", "dispute_stands", "findings_returned", "round_cap"])
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
class BuildTargetMissing extends TaggedError2()("BuildTargetMissing", {
|
|
275
|
+
message: String2
|
|
276
|
+
}) {
|
|
277
|
+
}
|
|
278
|
+
var outcomeGuidance = {
|
|
279
|
+
feature: "Deliver the whole scope; review checks it was met.",
|
|
280
|
+
bug: "Your failing test is already in place: make it pass.",
|
|
281
|
+
refactor: "Keep behaviour exactly as it is; the approved verifications must pass before and after.",
|
|
282
|
+
investigation: "Investigate and report what you found, with its sources.",
|
|
283
|
+
docs: "Name, in `documented`, the approved checks that run the commands you documented.",
|
|
284
|
+
migration: "Name, in `rollback`, the approved check that rolls the migration back."
|
|
285
|
+
};
|
|
286
|
+
var implementPrompt = (task, outcome) => [
|
|
287
|
+
`Build ${task}`,
|
|
288
|
+
...outcome === undefined ? [] : [outcomeGuidance[outcome]],
|
|
289
|
+
'Commit nothing yourself: Kitman pushes what you leave in the workspace. Give the ChangeRequest a title and a summary, and list as claims what you believe but Kitman has not checked, such as "the tests pass".'
|
|
290
|
+
].join(`
|
|
291
|
+
|
|
292
|
+
`);
|
|
293
|
+
var reproducePrompt = (task) => [
|
|
294
|
+
`Reproduce the bug in ${task} before anyone fixes it.`,
|
|
295
|
+
"Add a test that fails because of the bug, and change nothing else. Name the approved check that runs it; it must fail now and pass once the bug is fixed."
|
|
296
|
+
].join(`
|
|
297
|
+
|
|
298
|
+
`);
|
|
299
|
+
var findingLine = (finding) => `- **${finding.severity}** \`${finding.id}\` ${finding.detail}`;
|
|
300
|
+
var fixPrompt = (settled) => [
|
|
301
|
+
"The review found these. Fix each, or rebut it with your reason if it is wrong; reviewers judge the rebuttals next round.",
|
|
302
|
+
...settled.open.map(findingLine),
|
|
303
|
+
...settled.standing.map((entry) => findingLine(entry.finding))
|
|
304
|
+
].join(`
|
|
305
|
+
`);
|
|
306
|
+
var scopeFindings = (scope) => (scope?.unmet ?? []).map((item) => ({
|
|
307
|
+
id: `scope: ${item}`,
|
|
308
|
+
severity: "blocker",
|
|
309
|
+
file: "",
|
|
310
|
+
line: 0,
|
|
311
|
+
detail: "The reviewer found this part of the Task not done.",
|
|
312
|
+
seats: ["correctness"]
|
|
313
|
+
}));
|
|
314
|
+
var blockingIds = (settled) => settled.open.filter(isBlocking).map((finding) => finding.id);
|
|
315
|
+
var rally = (input) => gen(function* () {
|
|
316
|
+
const claims = [...input.claims];
|
|
317
|
+
let rebuttals = [];
|
|
318
|
+
let previous;
|
|
319
|
+
for (let round = 1;; round++) {
|
|
320
|
+
const head = yield* verify(`verify:${round}`, {});
|
|
321
|
+
const reviewed = yield* call(`review:${round}`, review_default, {
|
|
322
|
+
base: input.base,
|
|
323
|
+
head: head.revision,
|
|
324
|
+
...rebuttals.length === 0 ? undefined : { rebuttals },
|
|
325
|
+
...input.outcome === undefined ? undefined : { outcome: input.outcome },
|
|
326
|
+
claims: [...claims]
|
|
327
|
+
});
|
|
328
|
+
const settled = settleRound({
|
|
329
|
+
findings: [...reviewed.findings, ...scopeFindings(reviewed.scope)],
|
|
330
|
+
rebuttals,
|
|
331
|
+
verdicts: reviewed.verdicts
|
|
332
|
+
});
|
|
333
|
+
const decision = rallyStop({
|
|
334
|
+
round,
|
|
335
|
+
maxRounds: input.maxRounds,
|
|
336
|
+
settled,
|
|
337
|
+
...previous === undefined ? undefined : { previous }
|
|
338
|
+
});
|
|
339
|
+
if (decision.stop)
|
|
340
|
+
return { round, head, settled, decision, claims, scope: reviewed.scope };
|
|
341
|
+
const fixed = yield* agentWork(`fix:${round}`, {
|
|
342
|
+
agent: "implementer",
|
|
343
|
+
prompt: fixPrompt(settled),
|
|
344
|
+
output: Fixed
|
|
345
|
+
});
|
|
346
|
+
claims.push(...fixed.claims);
|
|
347
|
+
rebuttals = fixed.rebuttals;
|
|
348
|
+
previous = blockingIds(settled);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
var build_default = defineLoop({
|
|
352
|
+
id: "build",
|
|
353
|
+
title: "Build",
|
|
354
|
+
input: Input2,
|
|
355
|
+
output: Output2,
|
|
356
|
+
agents: { implementer: "implementer" },
|
|
357
|
+
checkout: "write",
|
|
358
|
+
offers: [{ id: "code-quality", title: "Improve code quality", loop: "code-quality" }],
|
|
359
|
+
capabilities: ["agent_work", "call", "verify", "change_request", "write_back"],
|
|
360
|
+
run: (input) => gen(function* () {
|
|
361
|
+
if (input.words === undefined && input.taskId === undefined)
|
|
362
|
+
return yield* new BuildTargetMissing({ message: "Give the words to build, or a Task" });
|
|
363
|
+
yield* writeBack("write-back:started", { milestone: "started" });
|
|
364
|
+
const task = input.words === undefined ? "the Task your brief describes." : `this: ${input.words}`;
|
|
365
|
+
const outcome = input.outcome;
|
|
366
|
+
const reproduced = outcome === "bug" ? yield* agentWork("reproduce", { agent: "implementer", prompt: reproducePrompt(task), output: Reproduced }) : undefined;
|
|
367
|
+
const base = outcome === "bug" ? yield* verify("verify:reproduction", {}) : outcome === "refactor" ? yield* verify("verify:base", {}) : undefined;
|
|
368
|
+
const implemented = yield* agentWork("implement", {
|
|
369
|
+
agent: "implementer",
|
|
370
|
+
prompt: implementPrompt(task, outcome),
|
|
371
|
+
output: Implemented
|
|
372
|
+
});
|
|
373
|
+
const last = yield* rally({
|
|
374
|
+
maxRounds: input.maxRounds ?? 3,
|
|
375
|
+
base: input.revision ?? `origin/${input.branch ?? "HEAD"}`,
|
|
376
|
+
outcome,
|
|
377
|
+
claims: implemented.claims
|
|
378
|
+
});
|
|
379
|
+
const verdict = evidenceGate({
|
|
380
|
+
...outcome === undefined ? undefined : { outcome },
|
|
381
|
+
head: last.head,
|
|
382
|
+
...base === undefined ? undefined : { base },
|
|
383
|
+
...last.scope === undefined ? undefined : { review: last.scope },
|
|
384
|
+
...reproduced === undefined ? undefined : { reproducing: reproduced.check },
|
|
385
|
+
...implemented.documented === undefined ? undefined : { documented: implemented.documented },
|
|
386
|
+
...implemented.rollback === undefined ? undefined : { rollback: implemented.rollback }
|
|
387
|
+
});
|
|
388
|
+
const unsettled = [...verdict.settled ? [] : verdict.reasons, ...unsettledItems(last.settled, last.decision)];
|
|
389
|
+
const opened = yield* changeRequest("change-request", {
|
|
390
|
+
title: implemented.title,
|
|
391
|
+
body: changeRequestBody({
|
|
392
|
+
summary: implemented.summary,
|
|
393
|
+
head: last.head,
|
|
394
|
+
claims: last.claims,
|
|
395
|
+
unsettled,
|
|
396
|
+
rounds: last.round,
|
|
397
|
+
stopped: last.decision.reason
|
|
398
|
+
})
|
|
399
|
+
});
|
|
400
|
+
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
401
|
+
return {
|
|
402
|
+
changeRequest: opened,
|
|
403
|
+
revision: last.head.revision,
|
|
404
|
+
tree: last.head.tree,
|
|
405
|
+
settled: unsettled.length === 0,
|
|
406
|
+
unsettled,
|
|
407
|
+
rounds: last.round,
|
|
408
|
+
stopped: last.decision.reason
|
|
409
|
+
};
|
|
410
|
+
}).pipe(tapError(() => ignore(writeBack("write-back:failed", { milestone: "failed" }))))
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// packages/loops/src/code-quality-pass.ts
|
|
414
|
+
var CodeQualityPasses = ["architecture", "simplification"];
|
|
415
|
+
var Pass = Literals(CodeQualityPasses);
|
|
416
|
+
var PassInput = Struct({
|
|
417
|
+
pass: Pass,
|
|
418
|
+
revision: optional(String2),
|
|
419
|
+
branch: optional(String2),
|
|
420
|
+
repoBindingId: optional(String2),
|
|
421
|
+
words: optional(String2)
|
|
422
|
+
});
|
|
423
|
+
var Improved = Struct({
|
|
424
|
+
improved: Boolean2,
|
|
425
|
+
title: optional(String2),
|
|
426
|
+
summary: String2,
|
|
427
|
+
claims: ArraySchema(String2)
|
|
428
|
+
});
|
|
429
|
+
var PassOutput = Struct({
|
|
430
|
+
pass: Pass,
|
|
431
|
+
improved: Boolean2,
|
|
432
|
+
changeRequest: optional(ChangeRequestResult),
|
|
433
|
+
revision: optional(String2),
|
|
434
|
+
tree: optional(String2),
|
|
435
|
+
settled: optional(Boolean2),
|
|
436
|
+
unsettled: optional(ArraySchema(String2))
|
|
437
|
+
});
|
|
438
|
+
var passes = {
|
|
439
|
+
architecture: {
|
|
440
|
+
title: "Improve the architecture",
|
|
441
|
+
task: "Find the one architecture improvement worth most here: deepen a shallow module, close a seam that leaks, or move behaviour to the data it uses."
|
|
442
|
+
},
|
|
443
|
+
simplification: {
|
|
444
|
+
title: "Simplify the code",
|
|
445
|
+
task: "Find the code that is harder to read than it needs to be: duplication, dead code, needless indirection or abstraction, and simplify it."
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var improvePrompt = (input) => [
|
|
449
|
+
passes[input.pass].task,
|
|
450
|
+
...input.words === undefined ? [] : [`Look at: ${input.words}`],
|
|
451
|
+
"Keep behaviour exactly as it is, and keep the change small enough to review in one sitting. If nothing is worth changing, change nothing and set improved to false.",
|
|
452
|
+
"Commit nothing yourself: Kitman pushes what you leave in the workspace. Give the ChangeRequest a title and a summary, and list as claims what you believe but Kitman has not checked."
|
|
453
|
+
].join(`
|
|
454
|
+
|
|
455
|
+
`);
|
|
456
|
+
var code_quality_pass_default = defineLoop({
|
|
457
|
+
id: "code-quality-pass",
|
|
458
|
+
title: "Code Quality pass",
|
|
459
|
+
input: PassInput,
|
|
460
|
+
output: PassOutput,
|
|
461
|
+
agents: { implementer: "implementer" },
|
|
462
|
+
checkout: "write",
|
|
463
|
+
capabilities: ["agent_work", "call", "verify", "change_request"],
|
|
464
|
+
run: (input) => gen(function* () {
|
|
465
|
+
const improvement = yield* agentWork("improve", {
|
|
466
|
+
agent: "implementer",
|
|
467
|
+
prompt: improvePrompt(input),
|
|
468
|
+
output: Improved
|
|
469
|
+
});
|
|
470
|
+
if (!improvement.improved)
|
|
471
|
+
return { pass: input.pass, improved: false };
|
|
472
|
+
const start = input.revision ?? `origin/${input.branch ?? "HEAD"}`;
|
|
473
|
+
const base = yield* verify("verify:base", { revision: start });
|
|
474
|
+
const head = yield* verify("verify", {});
|
|
475
|
+
const reviewed = yield* call("review", review_default, { base: start, head: head.revision });
|
|
476
|
+
const settled = settleRound({ findings: reviewed.findings, rebuttals: [], verdicts: reviewed.verdicts });
|
|
477
|
+
const decision = rallyStop({ round: 1, maxRounds: 1, settled });
|
|
478
|
+
const stopped = decision.stop ? decision.reason : "round_cap";
|
|
479
|
+
const verdict = evidenceGate({ outcome: "refactor", base, head });
|
|
480
|
+
const unsettled = [
|
|
481
|
+
...verdict.settled ? [] : verdict.reasons,
|
|
482
|
+
...decision.stop ? unsettledItems(settled, decision) : []
|
|
483
|
+
];
|
|
484
|
+
const opened = yield* changeRequest("change-request", {
|
|
485
|
+
title: improvement.title ?? passes[input.pass].title,
|
|
486
|
+
body: changeRequestBody({
|
|
487
|
+
summary: improvement.summary,
|
|
488
|
+
head,
|
|
489
|
+
claims: improvement.claims,
|
|
490
|
+
unsettled,
|
|
491
|
+
rounds: 1,
|
|
492
|
+
stopped
|
|
493
|
+
})
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
pass: input.pass,
|
|
497
|
+
improved: true,
|
|
498
|
+
changeRequest: opened,
|
|
499
|
+
revision: head.revision,
|
|
500
|
+
tree: head.tree,
|
|
501
|
+
settled: unsettled.length === 0,
|
|
502
|
+
unsettled
|
|
503
|
+
};
|
|
504
|
+
})
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// packages/loops/src/code-quality.ts
|
|
508
|
+
var Input3 = PassInput.mapFields(({ pass: _pass, ...fields }) => fields);
|
|
509
|
+
var Output3 = Struct({
|
|
510
|
+
improvements: ArraySchema(PassOutput)
|
|
511
|
+
});
|
|
512
|
+
var code_quality_default = defineLoop({
|
|
513
|
+
id: "code-quality",
|
|
514
|
+
title: "Code Quality",
|
|
515
|
+
input: Input3,
|
|
516
|
+
output: Output3,
|
|
517
|
+
agents: {},
|
|
518
|
+
checkout: "read",
|
|
519
|
+
capabilities: code_quality_pass_default.capabilities,
|
|
520
|
+
run: (input) => gen(function* () {
|
|
521
|
+
const improvements = [];
|
|
522
|
+
for (const pass of CodeQualityPasses) {
|
|
523
|
+
const result = yield* call(pass, code_quality_pass_default, { pass, ...input });
|
|
524
|
+
if (result.improved)
|
|
525
|
+
improvements.push(result);
|
|
526
|
+
}
|
|
527
|
+
return { improvements };
|
|
528
|
+
})
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// packages/loops/src/hello.ts
|
|
532
|
+
var hello_default = defineLoop({
|
|
533
|
+
id: "hello",
|
|
534
|
+
title: "Hello",
|
|
535
|
+
input: Struct({ name: String2, napSeconds: optional(Finite) }),
|
|
536
|
+
output: Struct({ greeting: String2, mood: String2 }),
|
|
537
|
+
agents: {},
|
|
538
|
+
checkout: "none",
|
|
539
|
+
capabilities: ["ask"],
|
|
540
|
+
run: ({ name, napSeconds }) => gen(function* () {
|
|
541
|
+
const greeting = yield* make3({
|
|
542
|
+
name: "greet",
|
|
543
|
+
success: String2,
|
|
544
|
+
execute: as(logInfo("hello_greeted", name), `Hello, ${name}`)
|
|
545
|
+
});
|
|
546
|
+
const mood = yield* ask("mood", { prompt: `${greeting}. How are you?`, options: ["good", "tired"] });
|
|
547
|
+
yield* sleep2({ name: "nap", duration: `${napSeconds ?? 2} seconds`, inMemoryThreshold: 0 });
|
|
548
|
+
return { greeting, mood };
|
|
549
|
+
})
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// packages/loops/src/incident.ts
|
|
553
|
+
var Input4 = Struct({
|
|
554
|
+
alert: Record(String2, Json),
|
|
555
|
+
repoBindingId: optional(String2)
|
|
556
|
+
});
|
|
557
|
+
var Conclusion = Struct({
|
|
558
|
+
summary: String2,
|
|
559
|
+
cause: String2,
|
|
560
|
+
sources: NonEmptyArray(String2),
|
|
561
|
+
fix: optional(String2)
|
|
562
|
+
});
|
|
563
|
+
var Output4 = Struct({
|
|
564
|
+
conclusionArtifactId: String2,
|
|
565
|
+
fix: Literals(["none", "declined", "started"]),
|
|
566
|
+
changeRequestUrl: optional(String2)
|
|
567
|
+
});
|
|
568
|
+
var alertTitle = (alert) => {
|
|
569
|
+
const title = alert.title ?? alert.name ?? alert.summary;
|
|
570
|
+
return isString(title) ? title : "Incident";
|
|
571
|
+
};
|
|
572
|
+
var investigatePrompt = (alert) => [
|
|
573
|
+
"Investigate this alert and conclude what happened and why.",
|
|
574
|
+
"```json",
|
|
575
|
+
JSON.stringify(alert, null, 2),
|
|
576
|
+
"```",
|
|
577
|
+
"Cite as sources the logs, metrics, commits or code that support your conclusion. Propose a fix only when the cause is in code Kitman can change."
|
|
578
|
+
].join(`
|
|
579
|
+
`);
|
|
580
|
+
var conclusionText = (conclusion) => [
|
|
581
|
+
conclusion.summary,
|
|
582
|
+
`Cause: ${conclusion.cause}`,
|
|
583
|
+
"Sources:",
|
|
584
|
+
...conclusion.sources.map((source) => `- ${source}`),
|
|
585
|
+
...conclusion.fix === undefined ? [] : [`Fix: ${conclusion.fix}`]
|
|
586
|
+
].join(`
|
|
587
|
+
`);
|
|
588
|
+
var incident_default = defineLoop({
|
|
589
|
+
id: "incident",
|
|
590
|
+
title: "Incident",
|
|
591
|
+
input: Input4,
|
|
592
|
+
output: Output4,
|
|
593
|
+
outcome: "investigation",
|
|
594
|
+
agents: { investigator: "investigator" },
|
|
595
|
+
checkout: "read",
|
|
596
|
+
when: [{ event: "monitoring.alert", input: { alert: "alert" } }],
|
|
597
|
+
capabilities: ["agent_work", "ask", "call"],
|
|
598
|
+
run: (input) => gen(function* () {
|
|
599
|
+
const conclusion = yield* agentWork("investigate", {
|
|
600
|
+
agent: "investigator",
|
|
601
|
+
prompt: investigatePrompt(input.alert),
|
|
602
|
+
output: Conclusion
|
|
603
|
+
});
|
|
604
|
+
const stored = yield* artifact("conclusion", {
|
|
605
|
+
kind: "investigation_conclusion",
|
|
606
|
+
title: alertTitle(input.alert),
|
|
607
|
+
text: conclusionText(conclusion),
|
|
608
|
+
data: { alert: input.alert, sources: conclusion.sources }
|
|
609
|
+
});
|
|
610
|
+
const conclusionArtifactId = stored.artifactId;
|
|
611
|
+
if (conclusion.fix === undefined)
|
|
612
|
+
return { conclusionArtifactId, fix: "none" };
|
|
613
|
+
const answer = yield* ask("start-fix", {
|
|
614
|
+
prompt: `${conclusion.summary}
|
|
615
|
+
|
|
616
|
+
Start a Build that fixes it as a bug: ${conclusion.fix}?`,
|
|
617
|
+
options: ["Start a fix", "Not now"]
|
|
618
|
+
});
|
|
619
|
+
if (answer === "Not now")
|
|
620
|
+
return { conclusionArtifactId, fix: "declined" };
|
|
621
|
+
const fixed = yield* call("fix", build_default, {
|
|
622
|
+
words: `${conclusion.fix}
|
|
623
|
+
|
|
624
|
+
${conclusionText(conclusion)}`,
|
|
625
|
+
...input.repoBindingId === undefined ? undefined : { repoBindingId: input.repoBindingId },
|
|
626
|
+
outcome: "bug"
|
|
627
|
+
});
|
|
628
|
+
return { conclusionArtifactId, fix: "started", changeRequestUrl: fixed.changeRequest.url };
|
|
629
|
+
})
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// packages/loops/src/plan.ts
|
|
633
|
+
var Input5 = Struct({
|
|
634
|
+
words: String2,
|
|
635
|
+
repoBindingId: optional(String2),
|
|
636
|
+
branch: optional(String2)
|
|
637
|
+
});
|
|
638
|
+
var Grilled = Struct({
|
|
639
|
+
summary: String2,
|
|
640
|
+
decisions: ArraySchema(String2)
|
|
641
|
+
});
|
|
642
|
+
var Plan = Struct({
|
|
643
|
+
title: String2,
|
|
644
|
+
spec: String2,
|
|
645
|
+
tickets: PlanTickets,
|
|
646
|
+
outOfScope: ArraySchema(String2)
|
|
647
|
+
});
|
|
648
|
+
var Opinion = Struct({ summary: String2, concerns: ArraySchema(String2) });
|
|
649
|
+
var TicketResult = Struct({
|
|
650
|
+
key: String2,
|
|
651
|
+
title: String2,
|
|
652
|
+
status: Literals(["merged", "failed", "skipped"]),
|
|
653
|
+
repoBindingId: optional(String2),
|
|
654
|
+
changeRequest: optional(ChangeRequestResult),
|
|
655
|
+
reason: optional(String2)
|
|
656
|
+
});
|
|
657
|
+
var Output5 = Struct({
|
|
658
|
+
title: String2,
|
|
659
|
+
outOfScope: ArraySchema(String2),
|
|
660
|
+
implemented: Boolean2,
|
|
661
|
+
tickets: ArraySchema(TicketResult),
|
|
662
|
+
deliveries: ArraySchema(ChangeRequestResult),
|
|
663
|
+
tasks: optional(CreateTasksResult.fields.tasks)
|
|
664
|
+
});
|
|
665
|
+
var menu = ["Implement now", "File as Tasks", "Second opinion", "Refine", "Finish"];
|
|
666
|
+
var onConflict = ["Rebuild on the integration branch", "Skip"];
|
|
667
|
+
var grillPrompt = (words) => [
|
|
668
|
+
`Plan this: ${words}`,
|
|
669
|
+
"Interview the user until you understand what they want: ask one question at a time with the Kitman MCP `ask` tool, and give options when the choices are clear. Ask about scope, constraints and what done looks like, not what you can read in the code.",
|
|
670
|
+
"Write no files. Summarize what you learned, and list each decision the user made."
|
|
671
|
+
].join(`
|
|
672
|
+
|
|
673
|
+
`);
|
|
674
|
+
var ticketRules = "Each ticket is one Build: a vertical slice an agent can implement, review and deliver as one ChangeRequest. Give it a short unique key, and list in blockedBy the keys of the tickets whose code it needs. Set repoBindingId only when it changes another repository than the plan. Set outcome to the kind of change: feature, bug, refactor, docs, migration or investigation.";
|
|
675
|
+
var planPrompt = (words, grilled) => [
|
|
676
|
+
`Write the spec and the tickets for: ${words}`,
|
|
677
|
+
`From the interview: ${grilled.summary}`,
|
|
678
|
+
...grilled.decisions.map((decision) => `- ${decision}`),
|
|
679
|
+
ticketRules,
|
|
680
|
+
"List in outOfScope what the plan deliberately leaves out, one item each; each holds the Builds of its tickets.",
|
|
681
|
+
"Write no files: your Output is the plan, and Kitman stores it."
|
|
682
|
+
].join(`
|
|
683
|
+
|
|
684
|
+
`);
|
|
685
|
+
var planText = (plan) => [
|
|
686
|
+
`# ${plan.title}`,
|
|
687
|
+
plan.spec,
|
|
688
|
+
...plan.outOfScope.length === 0 ? [] : ["## Out of scope", plan.outOfScope.map((item) => `- ${item}`).join(`
|
|
689
|
+
`)],
|
|
690
|
+
"## Tickets",
|
|
691
|
+
...plan.tickets.map(ticketText)
|
|
692
|
+
].join(`
|
|
693
|
+
|
|
694
|
+
`);
|
|
695
|
+
var ticketText = (ticket) => [
|
|
696
|
+
`### ${ticket.key}: ${ticket.title}`,
|
|
697
|
+
ticket.body,
|
|
698
|
+
...ticket.acceptanceCriteria.map((criterion) => `- [ ] ${criterion}`),
|
|
699
|
+
...ticket.blockedBy.length === 0 ? [] : [`Blocked by: ${ticket.blockedBy.join(", ")}`]
|
|
700
|
+
].join(`
|
|
701
|
+
|
|
702
|
+
`);
|
|
703
|
+
var opinionPrompt = (plan) => [
|
|
704
|
+
"Review this plan as a second opinion. Look for missing scope, tickets too big to review, wrong blockers and risks. Change nothing.",
|
|
705
|
+
planText(plan)
|
|
706
|
+
].join(`
|
|
707
|
+
|
|
708
|
+
`);
|
|
709
|
+
var refinePrompt = (plan, words) => [`Change the plan as the user asks: ${words}`, ticketRules, planText(plan)].join(`
|
|
710
|
+
|
|
711
|
+
`);
|
|
712
|
+
var storePlan = (plan, suffix) => gen(function* () {
|
|
713
|
+
yield* artifact(`spec${suffix}`, {
|
|
714
|
+
kind: "plan_spec",
|
|
715
|
+
title: plan.title,
|
|
716
|
+
text: plan.spec,
|
|
717
|
+
data: { outOfScope: [...plan.outOfScope] }
|
|
718
|
+
});
|
|
719
|
+
yield* artifact(`tickets${suffix}`, {
|
|
720
|
+
kind: "plan_tickets",
|
|
721
|
+
title: `Tickets for ${plan.title}`,
|
|
722
|
+
text: plan.tickets.map(ticketText).join(`
|
|
723
|
+
|
|
724
|
+
`),
|
|
725
|
+
data: plan.tickets.map(({ repoBindingId, outcome, ...ticket }) => ({
|
|
726
|
+
...ticket,
|
|
727
|
+
acceptanceCriteria: [...ticket.acceptanceCriteria],
|
|
728
|
+
blockedBy: [...ticket.blockedBy],
|
|
729
|
+
...repoBindingId === undefined ? undefined : { repoBindingId },
|
|
730
|
+
...outcome === undefined ? undefined : { outcome }
|
|
731
|
+
}))
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
var buildWords = (ticket) => [
|
|
735
|
+
ticket.title,
|
|
736
|
+
ticket.body,
|
|
737
|
+
...ticket.acceptanceCriteria.length === 0 ? [] : ["Acceptance criteria:", ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)]
|
|
738
|
+
].join(`
|
|
739
|
+
|
|
740
|
+
`);
|
|
741
|
+
var settled = (ticket, result) => ({
|
|
742
|
+
key: ticket.key,
|
|
743
|
+
title: ticket.title,
|
|
744
|
+
...result
|
|
745
|
+
});
|
|
746
|
+
var buildTicket = (ticket, start, taskId) => gen(function* () {
|
|
747
|
+
let head = start;
|
|
748
|
+
for (let attempt = 1;; attempt++) {
|
|
749
|
+
const suffix = attempt === 1 ? "" : `:${attempt}`;
|
|
750
|
+
const built = yield* call(`build:${ticket.key}${suffix}`, build_default, {
|
|
751
|
+
...taskId === undefined ? { words: buildWords(ticket) } : { taskId },
|
|
752
|
+
repoBindingId: head.repoBindingId,
|
|
753
|
+
branch: head.branch,
|
|
754
|
+
revision: head.revision
|
|
755
|
+
}).pipe(catchTag("ChildFailed", (failure) => succeed5(failure)));
|
|
756
|
+
if (built instanceof ChildFailed)
|
|
757
|
+
return settled(ticket, { status: "failed", repoBindingId: head.repoBindingId, reason: built.reason });
|
|
758
|
+
const merged = yield* mergeChangeRequest(`merge:${ticket.key}${suffix}`, {
|
|
759
|
+
changeRequestId: built.changeRequest.id
|
|
760
|
+
});
|
|
761
|
+
const delivered = { repoBindingId: head.repoBindingId, changeRequest: built.changeRequest };
|
|
762
|
+
if (merged.status === "merged")
|
|
763
|
+
return settled(ticket, { status: "merged", ...delivered });
|
|
764
|
+
const choice = yield* ask(`conflict:${ticket.key}${suffix}`, {
|
|
765
|
+
prompt: `The ChangeRequest for ${ticket.key} (${built.changeRequest.url}) conflicts with ${head.branch}. Rebuild ${ticket.key} on the integration branch as it is now, or skip it and the tickets it blocks?`,
|
|
766
|
+
options: onConflict
|
|
767
|
+
});
|
|
768
|
+
if (choice === "Skip")
|
|
769
|
+
return settled(ticket, { status: "failed", ...delivered, reason: `It conflicted with ${head.branch}` });
|
|
770
|
+
head = yield* integrationBranch(`rebuild-from:${ticket.key}:${attempt + 1}`, {
|
|
771
|
+
repoBindingId: head.repoBindingId
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
var deliveryBody = (plan, results) => {
|
|
776
|
+
const line = (result) => result.status === "merged" && result.changeRequest !== undefined ? `- **${result.key}** [${result.title}](${result.changeRequest.url}); its evidence is on that ChangeRequest` : `- **${result.key}** ${result.title}: ${result.reason ?? result.status}`;
|
|
777
|
+
const section = (title, status) => {
|
|
778
|
+
const lines = results.flatMap((result) => result.status === status ? [line(result)] : []);
|
|
779
|
+
return lines.length === 0 ? [] : [`## ${title}`, lines.join(`
|
|
780
|
+
`)];
|
|
781
|
+
};
|
|
782
|
+
return [
|
|
783
|
+
`Delivers the plan "${plan.title}".`,
|
|
784
|
+
...section("Merged", "merged"),
|
|
785
|
+
...section("Failed", "failed"),
|
|
786
|
+
...section("Skipped", "skipped")
|
|
787
|
+
].join(`
|
|
788
|
+
|
|
789
|
+
`);
|
|
790
|
+
};
|
|
791
|
+
var implement = (input, plan) => gen(function* () {
|
|
792
|
+
const created = yield* createTasks("tasks", { tickets: plan.tickets });
|
|
793
|
+
const taskOf = new Map(created.tasks.map((task) => [task.key, task.taskId]));
|
|
794
|
+
const results = new Map;
|
|
795
|
+
const waves = yield* planWaves(plan.tickets);
|
|
796
|
+
for (const [index, wave] of waves.entries()) {
|
|
797
|
+
const ready = [];
|
|
798
|
+
for (const ticket of wave) {
|
|
799
|
+
const unmerged = ticket.blockedBy.find((key) => results.get(key)?.status !== "merged");
|
|
800
|
+
if (unmerged === undefined)
|
|
801
|
+
ready.push(ticket);
|
|
802
|
+
else
|
|
803
|
+
results.set(ticket.key, settled(ticket, { status: "skipped", reason: `blocked by ${unmerged}, which was not merged` }));
|
|
804
|
+
}
|
|
805
|
+
const repoOf = (ticket) => ticket.repoBindingId ?? input.repoBindingId;
|
|
806
|
+
const heads = new Map;
|
|
807
|
+
for (const repo of new Set(ready.map(repoOf)))
|
|
808
|
+
heads.set(repo, yield* integrationBranch(`integration:wave-${index + 1}:${repo ?? "default"}`, repo === undefined ? {} : { repoBindingId: repo }));
|
|
809
|
+
const built = yield* forEach(ready.flatMap((ticket) => {
|
|
810
|
+
const head = heads.get(repoOf(ticket));
|
|
811
|
+
return head === undefined ? [] : [buildTicket(ticket, head, taskOf.get(ticket.key))];
|
|
812
|
+
}), (building) => building, { concurrency: "unbounded" });
|
|
813
|
+
for (const result of built)
|
|
814
|
+
results.set(result.key, result);
|
|
815
|
+
}
|
|
816
|
+
const ordered = plan.tickets.flatMap((ticket) => results.get(ticket.key) ?? []);
|
|
817
|
+
const repos = [
|
|
818
|
+
...new Set(ordered.flatMap((result) => result.status === "merged" ? result.repoBindingId ?? [] : []))
|
|
819
|
+
];
|
|
820
|
+
const deliveries = yield* forEach(repos, (repo) => changeRequest(`deliver:${repo}`, {
|
|
821
|
+
title: plan.title,
|
|
822
|
+
body: deliveryBody(plan, ordered.filter((result) => result.repoBindingId === repo || result.repoBindingId === undefined)),
|
|
823
|
+
integration: repo
|
|
824
|
+
}));
|
|
825
|
+
return { tickets: ordered, deliveries };
|
|
826
|
+
});
|
|
827
|
+
var plan_default = defineLoop({
|
|
828
|
+
id: "plan",
|
|
829
|
+
title: "Plan",
|
|
830
|
+
input: Input5,
|
|
831
|
+
output: Output5,
|
|
832
|
+
agents: { planner: "planner", reviewer: "reviewer" },
|
|
833
|
+
checkout: "read",
|
|
834
|
+
capabilities: [
|
|
835
|
+
"agent_work",
|
|
836
|
+
"ask",
|
|
837
|
+
"call",
|
|
838
|
+
"verify",
|
|
839
|
+
"change_request",
|
|
840
|
+
"write_back",
|
|
841
|
+
"create_tasks",
|
|
842
|
+
"merge_change_request"
|
|
843
|
+
],
|
|
844
|
+
run: (input) => gen(function* () {
|
|
845
|
+
const grilled = yield* agentWork("grill", { agent: "planner", prompt: grillPrompt(input.words), output: Grilled });
|
|
846
|
+
let plan = yield* agentWork("plan", { agent: "planner", prompt: planPrompt(input.words, grilled), output: Plan });
|
|
847
|
+
yield* storePlan(plan, "");
|
|
848
|
+
for (let round = 1;; round++) {
|
|
849
|
+
const choice = yield* ask(`next:${round}`, { prompt: `What next for the plan "${plan.title}"?`, options: menu });
|
|
850
|
+
switch (choice) {
|
|
851
|
+
case "Finish":
|
|
852
|
+
return { title: plan.title, outOfScope: plan.outOfScope, implemented: false, tickets: [], deliveries: [] };
|
|
853
|
+
case "Implement now":
|
|
854
|
+
return {
|
|
855
|
+
title: plan.title,
|
|
856
|
+
outOfScope: plan.outOfScope,
|
|
857
|
+
implemented: true,
|
|
858
|
+
...yield* implement(input, plan)
|
|
859
|
+
};
|
|
860
|
+
case "File as Tasks": {
|
|
861
|
+
const filed = yield* createTasks(`file-tasks:${round}`, { tickets: plan.tickets });
|
|
862
|
+
return {
|
|
863
|
+
title: plan.title,
|
|
864
|
+
outOfScope: plan.outOfScope,
|
|
865
|
+
implemented: false,
|
|
866
|
+
tickets: [],
|
|
867
|
+
deliveries: [],
|
|
868
|
+
tasks: filed.tasks
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
case "Second opinion": {
|
|
872
|
+
const opinion = yield* agentWork(`second-opinion:${round}`, {
|
|
873
|
+
agent: "reviewer",
|
|
874
|
+
prompt: opinionPrompt(plan),
|
|
875
|
+
output: Opinion
|
|
876
|
+
});
|
|
877
|
+
yield* artifact(`opinion:${round}`, {
|
|
878
|
+
kind: "plan_review",
|
|
879
|
+
title: `Second opinion on ${plan.title}`,
|
|
880
|
+
text: [opinion.summary, ...opinion.concerns.map((concern) => `- ${concern}`)].join(`
|
|
881
|
+
|
|
882
|
+
`)
|
|
883
|
+
});
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
case "Refine": {
|
|
887
|
+
const words = yield* ask(`refine:${round}`, { prompt: "What should change in the plan?", freeText: true });
|
|
888
|
+
plan = yield* agentWork(`refine-plan:${round}`, {
|
|
889
|
+
agent: "planner",
|
|
890
|
+
prompt: refinePrompt(plan, words),
|
|
891
|
+
output: Plan
|
|
892
|
+
});
|
|
893
|
+
yield* storePlan(plan, `:${round}`);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
})
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// packages/loops/src/release.ts
|
|
902
|
+
var Input6 = Struct({
|
|
903
|
+
tag: optional(String2),
|
|
904
|
+
revision: optional(String2),
|
|
905
|
+
deployEvent: optional(String2),
|
|
906
|
+
deployTimeoutMinutes: optional(Int)
|
|
907
|
+
});
|
|
908
|
+
var Drafted = Struct({
|
|
909
|
+
tag: String2,
|
|
910
|
+
title: String2,
|
|
911
|
+
notes: String2,
|
|
912
|
+
changeRequests: ArraySchema(String2)
|
|
913
|
+
});
|
|
914
|
+
var Output6 = Union([
|
|
915
|
+
Struct({ status: Literal("rejected"), tag: String2, notesArtifactId: String2 }),
|
|
916
|
+
Struct({
|
|
917
|
+
status: Literals(["deployed", "deploy_failed", "timed_out"]),
|
|
918
|
+
tag: String2,
|
|
919
|
+
notesArtifactId: String2,
|
|
920
|
+
revision: String2,
|
|
921
|
+
url: String2
|
|
922
|
+
})
|
|
923
|
+
]);
|
|
924
|
+
var draftPrompt = (tag) => [
|
|
925
|
+
"Draft the release notes for the ChangeRequests merged since the last release tag on the base branch.",
|
|
926
|
+
"Group them by what a user of the software notices, and name each ChangeRequest you cover.",
|
|
927
|
+
tag === undefined ? "Propose the next version tag, following the scheme earlier tags use." : `The release is tagged ${tag}.`
|
|
928
|
+
].join(`
|
|
929
|
+
|
|
930
|
+
`);
|
|
931
|
+
var reports = {
|
|
932
|
+
deployed: "deployed",
|
|
933
|
+
deploy_failed: "reported a failed deploy",
|
|
934
|
+
timed_out: "timed out waiting for the deploy"
|
|
935
|
+
};
|
|
936
|
+
var release_default = defineLoop({
|
|
937
|
+
id: "release",
|
|
938
|
+
title: "Release",
|
|
939
|
+
input: Input6,
|
|
940
|
+
output: Output6,
|
|
941
|
+
agents: { releaser: "releaser" },
|
|
942
|
+
checkout: "read",
|
|
943
|
+
capabilities: ["agent_work", "ask", "publish_release"],
|
|
944
|
+
run: (input) => gen(function* () {
|
|
945
|
+
const drafted = yield* agentWork("draft-notes", {
|
|
946
|
+
agent: "releaser",
|
|
947
|
+
prompt: draftPrompt(input.tag),
|
|
948
|
+
output: Drafted
|
|
949
|
+
});
|
|
950
|
+
const tag = input.tag ?? drafted.tag;
|
|
951
|
+
const notes = yield* artifact("notes", {
|
|
952
|
+
kind: "release_notes",
|
|
953
|
+
title: drafted.title,
|
|
954
|
+
text: drafted.notes,
|
|
955
|
+
data: { tag, changeRequests: drafted.changeRequests }
|
|
956
|
+
});
|
|
957
|
+
const approval = yield* ask("approve", {
|
|
958
|
+
prompt: `Release ${tag} with these notes?
|
|
959
|
+
|
|
960
|
+
${drafted.notes}`,
|
|
961
|
+
options: ["Approve", "Reject"]
|
|
962
|
+
});
|
|
963
|
+
if (approval === "Reject")
|
|
964
|
+
return { status: "rejected", tag, notesArtifactId: notes.artifactId };
|
|
965
|
+
const released = yield* release2("tag", {
|
|
966
|
+
tag,
|
|
967
|
+
...input.revision === undefined ? undefined : { revision: input.revision },
|
|
968
|
+
title: drafted.title,
|
|
969
|
+
notes: drafted.notes
|
|
970
|
+
});
|
|
971
|
+
const status = yield* waitFor("deploy", {
|
|
972
|
+
event: input.deployEvent ?? "deploy",
|
|
973
|
+
filter: { tag },
|
|
974
|
+
timeout: `${input.deployTimeoutMinutes ?? 60} minutes`
|
|
975
|
+
}).pipe(map3((deploy) => deploy.payload.status === "failed" ? "deploy_failed" : "deployed"), catchTag("WaitTimedOut", () => succeed5("timed_out")));
|
|
976
|
+
yield* artifact("report", {
|
|
977
|
+
kind: "release_report",
|
|
978
|
+
title: `${tag} ${reports[status]}`,
|
|
979
|
+
text: `${tag} at ${released.revision} ${reports[status]}. ${released.url}`,
|
|
980
|
+
data: { tag, status, revision: released.revision, url: released.url }
|
|
981
|
+
});
|
|
982
|
+
return { status, tag, notesArtifactId: notes.artifactId, revision: released.revision, url: released.url };
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// packages/loops/src/research.ts
|
|
987
|
+
var Input7 = Struct({
|
|
988
|
+
taskId: String2,
|
|
989
|
+
repoBindingId: optional(String2)
|
|
990
|
+
});
|
|
991
|
+
var Sources = NonEmptyArray(String2);
|
|
992
|
+
var Report = Struct({
|
|
993
|
+
title: String2,
|
|
994
|
+
summary: String2,
|
|
995
|
+
findings: ArraySchema(Struct({ finding: String2, sources: Sources })),
|
|
996
|
+
sources: Sources,
|
|
997
|
+
openQuestions: ArraySchema(String2),
|
|
998
|
+
nextSteps: ArraySchema(String2)
|
|
999
|
+
});
|
|
1000
|
+
var Output7 = Struct({ reportArtifactId: String2 });
|
|
1001
|
+
var prompt = [
|
|
1002
|
+
"Research the question this Task asks, using the `research` skill. The Task's title, description, Acceptance Criteria and comments are in your context.",
|
|
1003
|
+
"Cite primary sources for every finding: the code, official documentation, specifications and first-party APIs. Say what you read or ran and what it showed; separate what you established from what you suspect.",
|
|
1004
|
+
"Return the report as your Output, with `title` set to the Task's title. Do not write it into the repository."
|
|
1005
|
+
].join(`
|
|
1006
|
+
|
|
1007
|
+
`);
|
|
1008
|
+
var list = (items) => items.length === 0 ? ["None."] : items.map((item) => `- ${item}`);
|
|
1009
|
+
var reportMarkdown = (report) => [
|
|
1010
|
+
`# ${report.title}`,
|
|
1011
|
+
"",
|
|
1012
|
+
report.summary,
|
|
1013
|
+
"",
|
|
1014
|
+
"## Findings",
|
|
1015
|
+
"",
|
|
1016
|
+
...report.findings.flatMap(({ finding, sources }) => [`- ${finding}`, ...sources.map((source) => ` - ${source}`)]),
|
|
1017
|
+
"",
|
|
1018
|
+
"## Sources",
|
|
1019
|
+
"",
|
|
1020
|
+
...list(report.sources),
|
|
1021
|
+
"",
|
|
1022
|
+
"## Open questions",
|
|
1023
|
+
"",
|
|
1024
|
+
...list(report.openQuestions),
|
|
1025
|
+
"",
|
|
1026
|
+
"## Next steps",
|
|
1027
|
+
"",
|
|
1028
|
+
...list(report.nextSteps)
|
|
1029
|
+
].join(`
|
|
1030
|
+
`);
|
|
1031
|
+
var sample = {
|
|
1032
|
+
title: "Does the API cap request bodies?",
|
|
1033
|
+
summary: "It does, at 1 MB.",
|
|
1034
|
+
findings: [{ finding: "The server sets maxRequestBodySize to 1 MB.", sources: ["apps/api/src/server.ts:20"] }],
|
|
1035
|
+
sources: ["apps/api/src/server.ts:20"],
|
|
1036
|
+
openQuestions: [],
|
|
1037
|
+
nextSteps: []
|
|
1038
|
+
};
|
|
1039
|
+
var research_default = defineLoop({
|
|
1040
|
+
id: "research",
|
|
1041
|
+
title: "Research",
|
|
1042
|
+
input: Input7,
|
|
1043
|
+
output: Output7,
|
|
1044
|
+
outcome: "investigation",
|
|
1045
|
+
agents: { researcher: "researcher" },
|
|
1046
|
+
checkout: "read",
|
|
1047
|
+
capabilities: ["agent_work"],
|
|
1048
|
+
checks: [{ name: "reports", script: { input: { taskId: "task-1" }, outputs: { research: sample } } }],
|
|
1049
|
+
run: () => gen(function* () {
|
|
1050
|
+
const report = yield* agentWork("research", { agent: "researcher", prompt, output: Report });
|
|
1051
|
+
const { artifactId } = yield* artifact("report", {
|
|
1052
|
+
kind: "research_report",
|
|
1053
|
+
title: report.title,
|
|
1054
|
+
text: reportMarkdown(report),
|
|
1055
|
+
data: report
|
|
1056
|
+
});
|
|
1057
|
+
yield* note("summary", { text: report.summary, artifactId });
|
|
1058
|
+
return { reportArtifactId: artifactId };
|
|
1059
|
+
})
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// packages/loops/src/index.ts
|
|
1063
|
+
var shippedLoops = {
|
|
1064
|
+
build: build_default,
|
|
1065
|
+
"code-quality": code_quality_default,
|
|
1066
|
+
"code-quality-pass": code_quality_pass_default,
|
|
1067
|
+
hello: hello_default,
|
|
1068
|
+
incident: incident_default,
|
|
1069
|
+
plan: plan_default,
|
|
1070
|
+
release: release_default,
|
|
1071
|
+
research: research_default,
|
|
1072
|
+
review: review_default
|
|
1073
|
+
};
|
|
1074
|
+
export {
|
|
1075
|
+
build_default as build,
|
|
1076
|
+
code_quality_default as codeQuality,
|
|
1077
|
+
code_quality_pass_default as codeQualityPass,
|
|
1078
|
+
hello_default as hello,
|
|
1079
|
+
incident_default as incident,
|
|
1080
|
+
plan_default as plan,
|
|
1081
|
+
release_default as release,
|
|
1082
|
+
research_default as research,
|
|
1083
|
+
review_default as review,
|
|
1084
|
+
shippedLoops
|
|
1085
|
+
};
|