@kitman-sh/cli 0.10.52 → 0.10.54
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-x9dhxfts.js → chunk-148mjppg.js} +5 -5
- package/dist/{chunk-mej2bx5n.js → chunk-19aqk325.js} +4 -4
- package/dist/{chunk-jafetf7z.js → chunk-3d6abgjh.js} +2 -2
- package/dist/{chunk-pp1p3g6e.js → chunk-4kssrezn.js} +6 -1
- package/dist/{chunk-ctasxxz5.js → chunk-5j3cxsnp.js} +10 -10
- package/dist/{chunk-7thnf14y.js → chunk-5y5axze6.js} +609 -6
- package/dist/{chunk-mbctzx7e.js → chunk-61k67vxh.js} +76 -1389
- package/dist/{chunk-arvwar2c.js → chunk-7evwar6b.js} +1 -1
- package/dist/{chunk-dre9z977.js → chunk-c0wrcam4.js} +31 -32
- package/dist/{chunk-eg8f5ncv.js → chunk-dkpqwk0q.js} +7 -7
- package/dist/{chunk-r8p3nxcp.js → chunk-eerh2t20.js} +10 -10
- package/dist/chunk-g7vk5tjp.js +1109 -0
- package/dist/{chunk-vjmz23rs.js → chunk-gwcqz1bj.js} +1 -1
- package/dist/{chunk-wh1pa2aa.js → chunk-m32wb6t0.js} +245 -108
- package/dist/{chunk-3bj7sc3v.js → chunk-mmetyep8.js} +7 -7
- package/dist/{chunk-30397r95.js → chunk-mtgkvbk4.js} +4 -4
- package/dist/{chunk-jagzz148.js → chunk-my987gxf.js} +20 -24
- package/dist/{chunk-dzh9e6n2.js → chunk-q0yna8sw.js} +52 -35
- package/dist/{chunk-6vns3ej0.js → chunk-rjfxnk05.js} +5 -5
- package/dist/{chunk-g5bs3f6z.js → chunk-twp7aprz.js} +4 -6
- package/dist/{chunk-x0b92bjb.js → chunk-vtn088tq.js} +6 -6
- package/dist/{chunk-b6thzgc9.js → chunk-wczs5vrq.js} +2 -2
- package/dist/{chunk-11p4wf6n.js → chunk-xgwjcq4z.js} +3 -3
- package/dist/{chunk-fpmwm9bp.js → chunk-y48wz1sa.js} +157 -33
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-1mc9vgcr.js +0 -39
- package/dist/chunk-jfvq7s24.js +0 -145
- package/dist/chunk-txwt6r25.js +0 -1731
- package/dist/chunk-vyv0p5eq.js +0 -3183
- package/dist/chunk-z821b2gp.js +0 -164
package/dist/chunk-vyv0p5eq.js
DELETED
|
@@ -1,3183 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
isString2,
|
|
4
|
-
forEach,
|
|
5
|
-
succeed6,
|
|
6
|
-
void_3,
|
|
7
|
-
gen,
|
|
8
|
-
map8,
|
|
9
|
-
as,
|
|
10
|
-
catchTag,
|
|
11
|
-
catchTags,
|
|
12
|
-
catchIf,
|
|
13
|
-
tapError2,
|
|
14
|
-
ignore,
|
|
15
|
-
optionalKey,
|
|
16
|
-
optional,
|
|
17
|
-
Literal,
|
|
18
|
-
String4,
|
|
19
|
-
Boolean2,
|
|
20
|
-
Struct,
|
|
21
|
-
Record,
|
|
22
|
-
ArraySchema,
|
|
23
|
-
NonEmptyArray,
|
|
24
|
-
Literals,
|
|
25
|
-
isPattern,
|
|
26
|
-
Finite,
|
|
27
|
-
isGreaterThanOrEqualTo,
|
|
28
|
-
isLessThanOrEqualTo,
|
|
29
|
-
Int,
|
|
30
|
-
isMaxLength,
|
|
31
|
-
TaggedError2,
|
|
32
|
-
Json
|
|
33
|
-
} from "./chunk-mbctzx7e.js";
|
|
34
|
-
import"./chunk-arvwar2c.js";
|
|
35
|
-
import {
|
|
36
|
-
CheckName,
|
|
37
|
-
MeasurePlan
|
|
38
|
-
} from "./chunk-wh1pa2aa.js";
|
|
39
|
-
import {
|
|
40
|
-
LoopOutcome,
|
|
41
|
-
defineLoop,
|
|
42
|
-
failsOn,
|
|
43
|
-
evidenceGate,
|
|
44
|
-
ChildFailed,
|
|
45
|
-
agentWork,
|
|
46
|
-
agentWorkOnce,
|
|
47
|
-
ask,
|
|
48
|
-
call,
|
|
49
|
-
waitFor,
|
|
50
|
-
verify,
|
|
51
|
-
PlanTickets,
|
|
52
|
-
planWaves,
|
|
53
|
-
ChangeRequestResult,
|
|
54
|
-
RunFile,
|
|
55
|
-
CreateTasksResult,
|
|
56
|
-
ApproveChangeRequestResult,
|
|
57
|
-
TranscriptPoint,
|
|
58
|
-
changeRequest,
|
|
59
|
-
writeBack,
|
|
60
|
-
artifact,
|
|
61
|
-
note,
|
|
62
|
-
comment,
|
|
63
|
-
createTasks,
|
|
64
|
-
alertTask,
|
|
65
|
-
integrationBranch,
|
|
66
|
-
mergeChangeRequest,
|
|
67
|
-
approveChangeRequest,
|
|
68
|
-
closeChangeRequest,
|
|
69
|
-
pipelineMoved,
|
|
70
|
-
rerunPipeline,
|
|
71
|
-
reportPipeline,
|
|
72
|
-
release2,
|
|
73
|
-
transcripts,
|
|
74
|
-
propose,
|
|
75
|
-
projectActivity,
|
|
76
|
-
pipelineStatus,
|
|
77
|
-
reviewThreads,
|
|
78
|
-
openChangeRequests,
|
|
79
|
-
ownChangeRequests,
|
|
80
|
-
draftUpdate,
|
|
81
|
-
claim,
|
|
82
|
-
measure,
|
|
83
|
-
Finding,
|
|
84
|
-
MergedFinding,
|
|
85
|
-
Rebuttal,
|
|
86
|
-
RebuttalVerdict,
|
|
87
|
-
ScopeJudgement,
|
|
88
|
-
isBlocking,
|
|
89
|
-
mergeFindings,
|
|
90
|
-
settleRound,
|
|
91
|
-
rallyStop,
|
|
92
|
-
syncBase,
|
|
93
|
-
Continuing,
|
|
94
|
-
continuingBrief
|
|
95
|
-
} from "./chunk-txwt6r25.js";
|
|
96
|
-
import"./chunk-pp1p3g6e.js";
|
|
97
|
-
import"./chunk-1mc9vgcr.js";
|
|
98
|
-
import"./chunk-z821b2gp.js";
|
|
99
|
-
|
|
100
|
-
// packages/loops/src/change-request-body.ts
|
|
101
|
-
var scopeFindings = (scope) => (scope?.unmet ?? []).map((item) => ({
|
|
102
|
-
id: `scope: ${item}`,
|
|
103
|
-
severity: "blocker",
|
|
104
|
-
file: "",
|
|
105
|
-
line: 0,
|
|
106
|
-
detail: "The reviewer found this part of the Task not done.",
|
|
107
|
-
seats: ["code-review"]
|
|
108
|
-
}));
|
|
109
|
-
var brokenChecks = (head) => head.unchanged === true ? [] : head.checks.filter((check) => check.status === "failed" && !check.preExisting);
|
|
110
|
-
var isCheckFinding = (id) => id.startsWith("check: ");
|
|
111
|
-
var checkFindings = (head) => brokenChecks(head).map((check) => ({
|
|
112
|
-
id: `check: ${check.name}`,
|
|
113
|
-
severity: "blocker",
|
|
114
|
-
file: "",
|
|
115
|
-
line: 0,
|
|
116
|
-
detail: [
|
|
117
|
-
`\`${check.command}\` fails on the head and passes on the base.`,
|
|
118
|
-
...check.outputTail === undefined ? [] : ["It printed:", "```", check.outputTail, "```"]
|
|
119
|
-
].join(`
|
|
120
|
-
`),
|
|
121
|
-
seats: ["verify"]
|
|
122
|
-
}));
|
|
123
|
-
var unsettledItems = (settled, decision) => decision.unsettled.filter((id) => !isCheckFinding(id)).map((id) => {
|
|
124
|
-
const standing = settled.standing.find((entry) => entry.finding.id === id);
|
|
125
|
-
if (standing !== undefined)
|
|
126
|
-
return `Dispute stands on ${id}: ${standing.finding.detail} (rebutted: ${standing.rebuttal.reason})`;
|
|
127
|
-
return `Unresolved ${settled.open.find((finding) => finding.id === id)?.severity ?? "blocker"} at ${id}: ${settled.open.find((finding) => finding.id === id)?.detail ?? ""}`;
|
|
128
|
-
});
|
|
129
|
-
var evidenceTable = (head) => [
|
|
130
|
-
`Collected by a Runner for revision \`${head.revision}\`, tree \`${head.tree}\`; it holds for any revision with this tree.`,
|
|
131
|
-
"",
|
|
132
|
-
"| Check | Command | Result |",
|
|
133
|
-
"| --- | --- | --- |",
|
|
134
|
-
...head.checks.map((entry) => `| ${entry.name}${entry.suggested === true ? " (suggested by the agent)" : ""} | \`${entry.command}\` | ${entry.status}${entry.status === "failed" && (entry.preExisting || head.unchanged === true) ? " (also fails on the base branch)" : ""} |`),
|
|
135
|
-
...head.checks.some((entry) => entry.suggested === true) ? [
|
|
136
|
-
"",
|
|
137
|
-
"The repository has no approved verifications, so these are the agent's suggestions; check them, and approve the ones to keep with `kitman verify add`."
|
|
138
|
-
] : []
|
|
139
|
-
].join(`
|
|
140
|
-
`);
|
|
141
|
-
var changeRequestBody = (build) => [
|
|
142
|
-
build.summary,
|
|
143
|
-
"## Evidence",
|
|
144
|
-
evidenceTable(build.head),
|
|
145
|
-
...build.criteria === undefined || build.criteria.length === 0 ? [] : ["## Acceptance criteria", ...build.criteria.map((answer) => `- ${answer}`)],
|
|
146
|
-
"## Claims",
|
|
147
|
-
"What the agent says and Kitman has not checked.",
|
|
148
|
-
...build.claims.length === 0 ? ["None."] : build.claims.map((claim) => `- ${claim}`),
|
|
149
|
-
"## Unsettled",
|
|
150
|
-
...build.unsettled.length === 0 ? ["Nothing is unsettled."] : build.unsettled.map((item) => `- ${item}`),
|
|
151
|
-
...build.later === undefined || build.later.length === 0 ? [] : ["## Left for later", ...build.later.map((item) => `- ${item}`)],
|
|
152
|
-
"## Review",
|
|
153
|
-
`${build.rounds} review round${build.rounds === 1 ? "" : "s"}; the rally stopped: ${build.stopped.replaceAll("_", " ")}.`
|
|
154
|
-
].join(`
|
|
155
|
-
|
|
156
|
-
`);
|
|
157
|
-
|
|
158
|
-
// packages/loops/src/review.ts
|
|
159
|
-
var buildLoop = () => ({ id: "build", input: BuildInput, output: BuildOutput });
|
|
160
|
-
var seats = {
|
|
161
|
-
"code-review": {
|
|
162
|
-
skill: "code-review",
|
|
163
|
-
focus: "Review along two axes: Standards (does the change follow the repository's documented conventions?) and Spec (does it do what was asked?)."
|
|
164
|
-
},
|
|
165
|
-
quality: {
|
|
166
|
-
skill: "code-review-and-quality",
|
|
167
|
-
focus: "Review across correctness, readability, architecture, security and performance."
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
var seatNames = ["code-review", "quality"];
|
|
171
|
-
var scopeSeat = "code-review";
|
|
172
|
-
var choices = ["Post to the ChangeRequest", "Fix here", "Build from it", "Done"];
|
|
173
|
-
var Input = Struct({
|
|
174
|
-
changeRequestId: optional(String4),
|
|
175
|
-
base: optional(String4),
|
|
176
|
-
head: optional(String4),
|
|
177
|
-
files: optional(ArraySchema(String4)),
|
|
178
|
-
since: optional(String4),
|
|
179
|
-
rebuttals: optional(ArraySchema(Rebuttal)),
|
|
180
|
-
outcome: optional(LoopOutcome),
|
|
181
|
-
claims: optional(ArraySchema(String4)),
|
|
182
|
-
earlier: optional(ArraySchema(MergedFinding)),
|
|
183
|
-
failing: optional(ArraySchema(String4)),
|
|
184
|
-
next: optional(Literals(choices))
|
|
185
|
-
});
|
|
186
|
-
var FollowUpStatus = Literals(["fixed", "changed", "still_there"]);
|
|
187
|
-
var FollowUp = Struct({ id: String4, status: FollowUpStatus });
|
|
188
|
-
var SeatOutput = Struct({
|
|
189
|
-
summary: String4,
|
|
190
|
-
findings: ArraySchema(Finding),
|
|
191
|
-
verdicts: optional(ArraySchema(RebuttalVerdict)),
|
|
192
|
-
scope: optional(ScopeJudgement),
|
|
193
|
-
followUps: optional(ArraySchema(FollowUp))
|
|
194
|
-
});
|
|
195
|
-
var Output = Struct({
|
|
196
|
-
summary: String4,
|
|
197
|
-
findings: ArraySchema(MergedFinding),
|
|
198
|
-
followUps: ArraySchema(FollowUp),
|
|
199
|
-
verdicts: ArraySchema(RebuttalVerdict),
|
|
200
|
-
blocking: Boolean2,
|
|
201
|
-
artifactId: String4,
|
|
202
|
-
scope: optional(ScopeJudgement),
|
|
203
|
-
approval: optional(Struct({ head: String4, approved: Boolean2, reason: optional(String4) })),
|
|
204
|
-
next: optional(Struct({ choice: Literals(choices), detail: optional(String4) }))
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
class ReviewTargetMissing extends TaggedError2()("ReviewTargetMissing", {
|
|
208
|
-
message: String4
|
|
209
|
-
}) {
|
|
210
|
-
}
|
|
211
|
-
var targetOf = (input, head) => {
|
|
212
|
-
if (input.changeRequestId !== undefined)
|
|
213
|
-
return `ChangeRequest ${input.changeRequestId} at revision ${head}. Read it with \`kitman.changeRequests.get("${input.changeRequestId}")\` through Kitman's MCP, fetch its source branch, and review the diff of revision ${head} against its target branch, even if the branch has moved since. If the Runner cannot fetch that revision (a fork, say), report exactly that as your only finding, a blocker, and stop.`;
|
|
214
|
-
if (input.base !== undefined && input.head !== undefined)
|
|
215
|
-
return input.files === undefined ? `the change \`git diff ${input.base}...${input.head}\` in your checkout.` : [
|
|
216
|
-
`only these files of the change \`git diff ${input.base}...${input.head}\` in your checkout; the rest of it was reviewed and approved before:`,
|
|
217
|
-
...input.files.map((file) => `- ${file}`),
|
|
218
|
-
...input.since === undefined ? [] : [`Also review all of \`git diff ${input.since}..${input.head}\`, whichever files it touches.`]
|
|
219
|
-
].join(`
|
|
220
|
-
`);
|
|
221
|
-
return;
|
|
222
|
-
};
|
|
223
|
-
var scopePrompt = (input) => [
|
|
224
|
-
"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.",
|
|
225
|
-
...input.outcome === undefined ? [] : [`The Outcome to prove is ${input.outcome}.`],
|
|
226
|
-
...input.claims === undefined || input.claims.length === 0 ? [] : [
|
|
227
|
-
"The implementer claims it built these. Check them against the code rather than taking them on trust:",
|
|
228
|
-
...input.claims.map((claim) => `- ${claim}`)
|
|
229
|
-
]
|
|
230
|
-
].join(`
|
|
231
|
-
|
|
232
|
-
`);
|
|
233
|
-
var earlierPrompt = (earlier) => [
|
|
234
|
-
"An earlier review found these. Give each as a `followUps` entry with its id: fixed, changed (the code moved but the problem stands) or still_there. Do not report them again as findings; review only what changed since, for new problems.",
|
|
235
|
-
...earlier.map((finding) => `- ${finding.id} (${finding.severity}): ${finding.detail}`)
|
|
236
|
-
].join(`
|
|
237
|
-
`);
|
|
238
|
-
var promptFor = (seat, target, input) => [
|
|
239
|
-
`You are one of two reviewers. Follow your \`${seats[seat].skill}\` skill; if you do not have it: ${seats[seat].focus}`,
|
|
240
|
-
`Review ${target}`,
|
|
241
|
-
"Give `summary`: one sentence on whether it can merge and what matters most.",
|
|
242
|
-
"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. When it is one instance of a class of bug, name every other instance in the diff in its detail. Report nothing you have not checked in the code.",
|
|
243
|
-
"If the diff is too big to review in one go, review it directory by directory and say so in a finding on the first file you skipped.",
|
|
244
|
-
...seat === scopeSeat && input.files === undefined ? [scopePrompt(input)] : [],
|
|
245
|
-
...input.earlier === undefined || input.earlier.length === 0 ? [] : [earlierPrompt(input.earlier)],
|
|
246
|
-
...input.failing === undefined || input.failing.length === 0 ? [] : [
|
|
247
|
-
`These checks fail on this head and pass on the base: ${input.failing.join(", ")}. The implementer has their output, so do not report them as findings, and do not say it can merge while they fail.`
|
|
248
|
-
],
|
|
249
|
-
...input.rebuttals === undefined || input.rebuttals.length === 0 ? [] : [
|
|
250
|
-
"The implementer disputes these findings. For each, give a verdict: accepted if the rebuttal is right and the finding should be dropped.",
|
|
251
|
-
...input.rebuttals.map((rebuttal) => `- ${rebuttal.findingId}: ${rebuttal.reason}`)
|
|
252
|
-
]
|
|
253
|
-
].join(`
|
|
254
|
-
|
|
255
|
-
`);
|
|
256
|
-
var rank = { still_there: 0, changed: 1, fixed: 2 };
|
|
257
|
-
var followUpsOf = (earlier, answers) => earlier.map((finding) => {
|
|
258
|
-
const statuses = answers.flatMap((answer) => answer.id === finding.id ? [answer.status] : []);
|
|
259
|
-
return {
|
|
260
|
-
id: finding.id,
|
|
261
|
-
status: statuses.length === 0 ? "still_there" : statuses.reduce((worst, status) => rank[status] < rank[worst] ? status : worst)
|
|
262
|
-
};
|
|
263
|
-
});
|
|
264
|
-
var reviewText = (summary, findings, followUps) => [
|
|
265
|
-
`**Kitman review.** ${summary}`,
|
|
266
|
-
...findings.length === 0 ? [] : [
|
|
267
|
-
findings.map((finding) => `- **${finding.severity}** \`${finding.id}\` ${finding.detail} _(${finding.seats.join(", ")})_`).join(`
|
|
268
|
-
`)
|
|
269
|
-
],
|
|
270
|
-
...followUps.length === 0 ? [] : [
|
|
271
|
-
`Earlier findings:
|
|
272
|
-
${followUps.map((entry) => `- \`${entry.id}\` ${entry.status.replace("_", " ")}`).join(`
|
|
273
|
-
`)}`
|
|
274
|
-
]
|
|
275
|
-
].join(`
|
|
276
|
-
|
|
277
|
-
`);
|
|
278
|
-
var fixBrief = (changeRequestId, findings) => [
|
|
279
|
-
`Fix what a review of ChangeRequest ${changeRequestId} found, on its branch:`,
|
|
280
|
-
...findings.map((finding) => `- ${finding.id} (${finding.severity}): ${finding.detail}`)
|
|
281
|
-
].join(`
|
|
282
|
-
`);
|
|
283
|
-
var review_default = defineLoop({
|
|
284
|
-
id: "review",
|
|
285
|
-
version: "3.3.0",
|
|
286
|
-
title: "Review",
|
|
287
|
-
input: Input,
|
|
288
|
-
output: Output,
|
|
289
|
-
agents: { "code-review": "code-reviewer", quality: "quality-reviewer" },
|
|
290
|
-
checkout: "read",
|
|
291
|
-
capabilities: [
|
|
292
|
-
"agent_work",
|
|
293
|
-
"post_comment",
|
|
294
|
-
"ask",
|
|
295
|
-
"call",
|
|
296
|
-
"verify",
|
|
297
|
-
"change_request",
|
|
298
|
-
"write_back",
|
|
299
|
-
"read_change_request",
|
|
300
|
-
"merge_change_request",
|
|
301
|
-
"approve_change_request"
|
|
302
|
-
],
|
|
303
|
-
calls: () => [build_default],
|
|
304
|
-
offers: [
|
|
305
|
-
{ id: "build", title: "Build from it", loop: "build" },
|
|
306
|
-
{ id: "review", title: "Review again", loop: "review" }
|
|
307
|
-
],
|
|
308
|
-
run: (input) => gen(function* () {
|
|
309
|
-
const head = input.changeRequestId === undefined ? undefined : (yield* pipelineStatus("head", { changeRequestId: input.changeRequestId })).head;
|
|
310
|
-
const target = targetOf(input, head);
|
|
311
|
-
if (target === undefined)
|
|
312
|
-
return yield* new ReviewTargetMissing({ message: "Name a ChangeRequest, or both base and head revisions" });
|
|
313
|
-
const outputs = yield* forEach(seatNames, (seat) => agentWork(`review:${seat}`, {
|
|
314
|
-
agent: seat,
|
|
315
|
-
prompt: promptFor(seat, target, input),
|
|
316
|
-
output: SeatOutput
|
|
317
|
-
}).pipe(map8((output) => [seat, output])), { concurrency: "unbounded" });
|
|
318
|
-
const earlier = input.earlier ?? [];
|
|
319
|
-
const earlierIds = new Set(earlier.map((finding) => finding.id));
|
|
320
|
-
const followUps = followUpsOf(earlier, outputs.flatMap(([, output]) => output.followUps ?? []));
|
|
321
|
-
const standing = new Set(followUps.flatMap((entry) => entry.status === "fixed" ? [] : [entry.id]));
|
|
322
|
-
const carried = earlier.filter((finding) => standing.has(finding.id));
|
|
323
|
-
const fresh = mergeFindings(Object.fromEntries(outputs.map(([seat, output]) => [
|
|
324
|
-
seat,
|
|
325
|
-
output.findings.filter((finding) => !earlierIds.has(`${finding.file}:${finding.line}`))
|
|
326
|
-
])));
|
|
327
|
-
const findings = [...carried, ...fresh];
|
|
328
|
-
const summary = findings.length === 0 ? "No findings: nothing stands in the way of merging." : outputs.map(([, output]) => output.summary).join(" ");
|
|
329
|
-
const freshIds = new Set(fresh.map((finding) => finding.id));
|
|
330
|
-
const text = reviewText(summary, findings, followUps);
|
|
331
|
-
const scope = input.files === undefined ? outputs.find(([seat]) => seat === scopeSeat)?.[1].scope : undefined;
|
|
332
|
-
const stored = yield* artifact("findings", {
|
|
333
|
-
kind: "review-findings",
|
|
334
|
-
title: "Review findings",
|
|
335
|
-
text,
|
|
336
|
-
data: { findings, followUps }
|
|
337
|
-
});
|
|
338
|
-
const reviewed = {
|
|
339
|
-
summary,
|
|
340
|
-
findings,
|
|
341
|
-
followUps,
|
|
342
|
-
verdicts: outputs.flatMap(([, output]) => output.verdicts ?? []),
|
|
343
|
-
blocking: findings.some(isBlocking),
|
|
344
|
-
artifactId: stored.artifactId,
|
|
345
|
-
...scope === undefined ? undefined : { scope }
|
|
346
|
-
};
|
|
347
|
-
const changeRequestId = input.changeRequestId;
|
|
348
|
-
if (changeRequestId === undefined || head === undefined)
|
|
349
|
-
return reviewed;
|
|
350
|
-
const approval = reviewed.blocking || scope?.met === false || (input.failing?.length ?? 0) > 0 ? undefined : yield* approveChangeRequest("approve", { changeRequestId, head, summary }).pipe(map8((result) => result.approved ? { head, approved: true } : { head, approved: false, reason: result.reason }));
|
|
351
|
-
const answer = input.next ?? (yield* ask("next", {
|
|
352
|
-
prompt: `${summary}
|
|
353
|
-
|
|
354
|
-
What next?`,
|
|
355
|
-
options: [
|
|
356
|
-
{ value: "Post to the ChangeRequest", description: "Comments the findings on the ChangeRequest." },
|
|
357
|
-
{ value: "Fix here", description: "A Build fixes them and pushes to this ChangeRequest." },
|
|
358
|
-
{ value: "Build from it", description: "A Build fixes them in a new ChangeRequest." },
|
|
359
|
-
{ value: "Done", description: "Keeps the review as an artifact and stops." }
|
|
360
|
-
],
|
|
361
|
-
recommended: reviewed.blocking ? "Fix here" : "Done",
|
|
362
|
-
recommendationReason: reviewed.blocking ? "A blocking finding stops the merge, and fixing it here is the shortest way through." : "Nothing blocks the merge, so the findings can wait in the Run.",
|
|
363
|
-
default: "Done",
|
|
364
|
-
timeout: "1 hour"
|
|
365
|
-
}));
|
|
366
|
-
const choice = choices.find((option) => option === answer) ?? "Done";
|
|
367
|
-
const next = yield* gen(function* () {
|
|
368
|
-
if (choice === "Post to the ChangeRequest") {
|
|
369
|
-
yield* comment("post", { changeRequestId, body: text, summary: "kitman-review" });
|
|
370
|
-
const anchors = findings.flatMap((finding) => !freshIds.has(finding.id) || finding.line < 1 ? [] : [{ file: finding.file, line: finding.line, body: `**${finding.severity}:** ${finding.detail}` }]);
|
|
371
|
-
if (anchors.length === 0)
|
|
372
|
-
return { choice };
|
|
373
|
-
return yield* comment("anchors", { changeRequestId, body: "", anchors }).pipe(as({ choice }), catchTag("StepFailed", (error) => succeed6({ choice, detail: `Posted the summary; the line comments failed: ${error.reason}` })));
|
|
374
|
-
}
|
|
375
|
-
if (choice === "Fix here") {
|
|
376
|
-
const fixed = yield* call("fix", buildLoop(), {
|
|
377
|
-
words: fixBrief(changeRequestId, findings),
|
|
378
|
-
changeRequestId,
|
|
379
|
-
maxRounds: 2
|
|
380
|
-
});
|
|
381
|
-
return { choice, detail: fixed.settled ? "Fixed and pushed" : `Pushed; left: ${fixed.unsettled.join("; ")}` };
|
|
382
|
-
}
|
|
383
|
-
if (choice === "Build from it") {
|
|
384
|
-
const built = yield* call("build", buildLoop(), { words: fixBrief(changeRequestId, findings) });
|
|
385
|
-
return { choice, detail: built.changeRequest.url };
|
|
386
|
-
}
|
|
387
|
-
return { choice, detail: "The review is kept as an artifact" };
|
|
388
|
-
}).pipe(catchTag("ChildFailed", (failed) => succeed6({ choice, detail: failed.reason })));
|
|
389
|
-
return { ...reviewed, ...approval === undefined ? undefined : { approval }, next };
|
|
390
|
-
})
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// packages/loops/src/shepherd-output.ts
|
|
394
|
-
var ShepherdOutput = Struct({
|
|
395
|
-
status: Literals(["merged", "ready", "needs_you"]),
|
|
396
|
-
reason: String4,
|
|
397
|
-
fixRounds: Int,
|
|
398
|
-
replies: Int
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// packages/loops/src/shepherd.ts
|
|
402
|
-
var reviewLoop = () => ({ id: "review", input: Input, output: Output });
|
|
403
|
-
var stepBudget = "45 minutes";
|
|
404
|
-
var Input2 = Struct({
|
|
405
|
-
changeRequestId: String4,
|
|
406
|
-
taskId: optional(String4),
|
|
407
|
-
merge: optional(Boolean2),
|
|
408
|
-
branch: optional(String4),
|
|
409
|
-
continuing: optional(Continuing),
|
|
410
|
-
maxFixRounds: optional(Int),
|
|
411
|
-
maxCycles: optional(Int)
|
|
412
|
-
});
|
|
413
|
-
var Output2 = ShepherdOutput;
|
|
414
|
-
var CiFix = Struct({
|
|
415
|
-
verdict: Literals(["fixed", "pre_existing", "flaky"]),
|
|
416
|
-
summary: String4
|
|
417
|
-
});
|
|
418
|
-
var Replies = Struct({
|
|
419
|
-
replies: ArraySchema(Struct({
|
|
420
|
-
threadId: String4,
|
|
421
|
-
reply: String4,
|
|
422
|
-
changed: Boolean2
|
|
423
|
-
}))
|
|
424
|
-
});
|
|
425
|
-
var failedJobs = (pipeline) => pipeline.jobs.flatMap((job) => job.status === "failed" ? [`### ${job.name}${job.url === undefined ? "" : ` (${job.url})`}
|
|
426
|
-
|
|
427
|
-
${job.logTail ?? "No log was served."}`] : []);
|
|
428
|
-
var ciFixPrompt = (pipeline, target) => [
|
|
429
|
-
`The pipeline at ${pipeline.head} failed. These jobs failed, with the end of each log:`,
|
|
430
|
-
...failedJobs(pipeline),
|
|
431
|
-
"Run the failing job once more here to rule out a flake; if it passes, change nothing and answer flaky.",
|
|
432
|
-
`Then run it on the target branch (a worktree of \`origin/${target}\`, which Kitman fetched just before this Step; do not fetch). If it fails there too, the branch did not cause it: change nothing and answer pre_existing.`,
|
|
433
|
-
"Otherwise it is this branch's own failure: write or fix a test that shows it first, then fix it, and answer fixed. Kitman pushes what you leave."
|
|
434
|
-
].join(`
|
|
435
|
-
|
|
436
|
-
`);
|
|
437
|
-
var threadLine = (thread) => [
|
|
438
|
-
`### Thread ${thread.id}${thread.path === undefined ? "" : ` on ${thread.path}${thread.line === undefined ? "" : `:${thread.line}`}`}`,
|
|
439
|
-
...thread.comments.map((entry) => `${entry.author ?? "someone"}: ${entry.body}`)
|
|
440
|
-
].join(`
|
|
441
|
-
`);
|
|
442
|
-
var ReviewFix = Struct({ summary: String4 });
|
|
443
|
-
var reviewFixPrompt = (findings) => [
|
|
444
|
-
"A review of the files the catch-up merge changed found these. Fix each blocker and major finding, with a test that shows it where one can. Kitman pushes what you leave.",
|
|
445
|
-
...findings.map((finding) => `- ${finding.id} (${finding.severity}): ${finding.detail}`)
|
|
446
|
-
].join(`
|
|
447
|
-
`);
|
|
448
|
-
var catchUpVerdict = (catchUp) => {
|
|
449
|
-
if (catchUp.truncated)
|
|
450
|
-
return { kind: "changed", why: "it changed more files than the Runner compares" };
|
|
451
|
-
const files = [...new Set([...catchUp.conflicted, ...catchUp.rewritten])];
|
|
452
|
-
const elsewhere = catchUp.changed.filter((file) => !files.includes(file));
|
|
453
|
-
if (elsewhere.length > 0)
|
|
454
|
-
return { kind: "changed", why: `the branch's own diff changed in ${elsewhere.join(", ")}` };
|
|
455
|
-
return files.length === 0 ? { kind: "carry" } : { kind: "review", files };
|
|
456
|
-
};
|
|
457
|
-
var repliesPrompt = (threads) => [
|
|
458
|
-
"Reviewers left these comments on the ChangeRequest. For each thread, apply what you agree with and give the reply to post on it: what you changed, or why you did not. A comment that asks a question gets an answer and no change.",
|
|
459
|
-
"Never mark a thread resolved: whether a disputed point is settled is the reviewer's call.",
|
|
460
|
-
...threads.map(threadLine)
|
|
461
|
-
].join(`
|
|
462
|
-
|
|
463
|
-
`);
|
|
464
|
-
var threadKey = (thread) => `${thread.id}:${thread.comments.length}`;
|
|
465
|
-
var shepherd_default = defineLoop({
|
|
466
|
-
id: "shepherd",
|
|
467
|
-
version: "1.1.0",
|
|
468
|
-
title: "Shepherd",
|
|
469
|
-
input: Input2,
|
|
470
|
-
output: Output2,
|
|
471
|
-
agents: { implementer: "implementer" },
|
|
472
|
-
checkout: "write",
|
|
473
|
-
capabilities: [
|
|
474
|
-
"agent_work",
|
|
475
|
-
"read_change_request",
|
|
476
|
-
"post_comment",
|
|
477
|
-
"merge_change_request",
|
|
478
|
-
"approve_change_request",
|
|
479
|
-
"call",
|
|
480
|
-
"write_back",
|
|
481
|
-
"ask",
|
|
482
|
-
"verify"
|
|
483
|
-
],
|
|
484
|
-
calls: () => [review_default],
|
|
485
|
-
run: (input) => {
|
|
486
|
-
const changeRequestId = input.changeRequestId;
|
|
487
|
-
const target = input.continuing?.targetBranch ?? input.branch ?? "HEAD";
|
|
488
|
-
const answered = new Set;
|
|
489
|
-
const reranHeads = new Set;
|
|
490
|
-
const ranOnRunner = new Set;
|
|
491
|
-
let fixRounds = 0;
|
|
492
|
-
let replies = 0;
|
|
493
|
-
let stalledAt;
|
|
494
|
-
let stalls = 0;
|
|
495
|
-
let unreviewed;
|
|
496
|
-
let notCarried;
|
|
497
|
-
const done = (status, reason) => ({ status, reason, fixRounds, replies });
|
|
498
|
-
const runOnRunner = (step, cycle, head, checks) => gen(function* () {
|
|
499
|
-
const verification = `pipeline-${step}:${cycle}`;
|
|
500
|
-
const failed = yield* verify(verification, { revision: head, checks }).pipe(as(undefined), catchTag("StepFailed", (error) => succeed6(error.reason)));
|
|
501
|
-
if (failed === undefined)
|
|
502
|
-
yield* reportPipeline(`pipeline-${step === "run" ? "report" : "rereport"}:${cycle}`, {
|
|
503
|
-
changeRequestId,
|
|
504
|
-
verification
|
|
505
|
-
});
|
|
506
|
-
return failed;
|
|
507
|
-
});
|
|
508
|
-
return gen(function* () {
|
|
509
|
-
for (let cycle = 1;; cycle++) {
|
|
510
|
-
if (cycle > (input.maxCycles ?? 20)) {
|
|
511
|
-
const goOn = yield* ask(`continue:${cycle}`, {
|
|
512
|
-
prompt: `Shepherd has looked at this ChangeRequest ${cycle - 1} times without landing it. Keep going?`,
|
|
513
|
-
options: ["Keep going", "Stop"],
|
|
514
|
-
recommended: "Stop",
|
|
515
|
-
recommendationReason: `${cycle - 1} looks have not landed it, so something needs a person rather than another look.`,
|
|
516
|
-
default: "Stop",
|
|
517
|
-
timeout: "1 hour"
|
|
518
|
-
});
|
|
519
|
-
if (goOn === "Stop")
|
|
520
|
-
return done("needs_you", `Stopped after ${cycle - 1} looks without landing it`);
|
|
521
|
-
}
|
|
522
|
-
const pipeline = yield* pipelineStatus(`pipeline:${cycle}`, { changeRequestId });
|
|
523
|
-
if (pipeline.status === "none" && pipeline.runnerChecks !== undefined) {
|
|
524
|
-
if (ranOnRunner.has(pipeline.head))
|
|
525
|
-
return done("needs_you", `The pipeline ran on the Runner at ${pipeline.head}, but no result was recorded`);
|
|
526
|
-
ranOnRunner.add(pipeline.head);
|
|
527
|
-
const failed = yield* runOnRunner("run", cycle, pipeline.head, pipeline.runnerChecks);
|
|
528
|
-
if (failed !== undefined)
|
|
529
|
-
return done("needs_you", `The pipeline could not run on the Runner: ${failed}`);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
if (pipeline.status === "none" || pipeline.status === "running") {
|
|
533
|
-
yield* pipelineMoved(`wait:${cycle}`, changeRequestId, pipeline);
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
if (pipeline.status === "failed" || pipeline.status === "canceled") {
|
|
537
|
-
if (stalledAt === pipeline.head)
|
|
538
|
-
stalls++;
|
|
539
|
-
else
|
|
540
|
-
stalls = 0;
|
|
541
|
-
if (stalls >= 2)
|
|
542
|
-
return done("needs_you", `Two fixes pushed nothing new; the pipeline at ${pipeline.head} still fails`);
|
|
543
|
-
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
544
|
-
return done("needs_you", `The pipeline still fails after ${fixRounds} fix rounds`);
|
|
545
|
-
fixRounds++;
|
|
546
|
-
stalledAt = pipeline.head;
|
|
547
|
-
const fixed = yield* agentWork(`ci-fix:${cycle}`, {
|
|
548
|
-
agent: "implementer",
|
|
549
|
-
prompt: ciFixPrompt(pipeline, target),
|
|
550
|
-
output: CiFix,
|
|
551
|
-
timeout: stepBudget
|
|
552
|
-
});
|
|
553
|
-
if (fixed.verdict === "pre_existing")
|
|
554
|
-
return done("needs_you", `The failure is pre-existing on the target branch: ${fixed.summary}`);
|
|
555
|
-
if (fixed.verdict === "flaky") {
|
|
556
|
-
if (pipeline.source === "reported") {
|
|
557
|
-
if (pipeline.runnerChecks === undefined)
|
|
558
|
-
return done("needs_you", `The failing job passed when run again, outside the forge: ${fixed.summary}`);
|
|
559
|
-
if (reranHeads.has(pipeline.head))
|
|
560
|
-
return done("needs_you", `The failing job failed again when run again on the Runner: ${fixed.summary}`);
|
|
561
|
-
reranHeads.add(pipeline.head);
|
|
562
|
-
const failed = yield* runOnRunner("rerun", cycle, pipeline.head, pipeline.runnerChecks);
|
|
563
|
-
if (failed !== undefined)
|
|
564
|
-
return done("needs_you", `The pipeline could not run on the Runner: ${failed}`);
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
if (reranHeads.has(pipeline.head))
|
|
568
|
-
return done("needs_you", `The failing job failed again when run again on the forge: ${fixed.summary}`);
|
|
569
|
-
reranHeads.add(pipeline.head);
|
|
570
|
-
yield* rerunPipeline(`rerun:${cycle}`, { changeRequestId });
|
|
571
|
-
yield* pipelineMoved(`rerun-wait:${cycle}`, changeRequestId, pipeline).pipe(catchTag("WaitTimedOut", () => void_3));
|
|
572
|
-
}
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
const threads = (yield* reviewThreads(`threads:${cycle}`, { changeRequestId })).threads.filter((thread) => !answered.has(threadKey(thread)));
|
|
576
|
-
if (threads.length > 0) {
|
|
577
|
-
const answers = yield* agentWork(`comments:${cycle}`, {
|
|
578
|
-
agent: "implementer",
|
|
579
|
-
prompt: repliesPrompt(threads),
|
|
580
|
-
output: Replies,
|
|
581
|
-
timeout: stepBudget
|
|
582
|
-
});
|
|
583
|
-
for (const answer of answers.replies) {
|
|
584
|
-
yield* comment(`reply:${cycle}:${answer.threadId}`, {
|
|
585
|
-
changeRequestId,
|
|
586
|
-
threadId: answer.threadId,
|
|
587
|
-
body: answer.reply
|
|
588
|
-
});
|
|
589
|
-
replies++;
|
|
590
|
-
}
|
|
591
|
-
for (const thread of threads)
|
|
592
|
-
answered.add(`${thread.id}:${thread.comments.length + 1}`);
|
|
593
|
-
if (answers.replies.some((answer) => answer.changed))
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
const commented = {
|
|
597
|
-
event: "change_request.comment",
|
|
598
|
-
filter: { changeRequestId },
|
|
599
|
-
...pipeline.readAt === undefined ? undefined : { since: pipeline.readAt }
|
|
600
|
-
};
|
|
601
|
-
const mergeable = yield* waitFor(`mergeable:${cycle}`, {
|
|
602
|
-
event: "change_request.mergeable",
|
|
603
|
-
filter: { changeRequestId },
|
|
604
|
-
timeout: "1 hour",
|
|
605
|
-
or: [commented]
|
|
606
|
-
});
|
|
607
|
-
if (mergeable.type === commented.event)
|
|
608
|
-
continue;
|
|
609
|
-
if (mergeable.payload.mergeable === false) {
|
|
610
|
-
const synced = yield* syncBase(`sync:${cycle}`, { agent: "implementer", target });
|
|
611
|
-
if (synced.status === "conflict")
|
|
612
|
-
return done("needs_you", `Merging ${target} in conflicts on ${synced.files.join(", ")}: ${synced.reason}`);
|
|
613
|
-
if (synced.status === "merged") {
|
|
614
|
-
const caught = synced.catchUp;
|
|
615
|
-
const verdict = caught === undefined ? { kind: "changed", why: "its Runner did not report what it did to the branch" } : catchUpVerdict(caught);
|
|
616
|
-
if (verdict.kind === "changed") {
|
|
617
|
-
unreviewed = undefined;
|
|
618
|
-
notCarried = `merging ${target} in changed it: ${verdict.why}`;
|
|
619
|
-
} else if (caught !== undefined && unreviewed !== undefined) {
|
|
620
|
-
const files = [...new Set([...unreviewed.files, ...verdict.kind === "review" ? verdict.files : []])];
|
|
621
|
-
unreviewed = caught.from === unreviewed.synced ? { ...unreviewed, files, synced: caught.head } : { ...unreviewed, files };
|
|
622
|
-
} else if (caught !== undefined && verdict.kind === "review") {
|
|
623
|
-
notCarried = undefined;
|
|
624
|
-
unreviewed = { files: verdict.files, synced: caught.head, sync: `sync:${cycle}`, earlier: [] };
|
|
625
|
-
} else if (caught !== undefined) {
|
|
626
|
-
notCarried = undefined;
|
|
627
|
-
const carried = yield* approveChangeRequest(`carry:${cycle}`, {
|
|
628
|
-
changeRequestId,
|
|
629
|
-
head: caught.head,
|
|
630
|
-
summary: `Merging ${target} in left the branch's own diff as it was approved.`,
|
|
631
|
-
carries: `sync:${cycle}`
|
|
632
|
-
}).pipe(catchTag("StepFailed", (error) => succeed6({ approved: false, reason: error.reason })));
|
|
633
|
-
if (!carried.approved)
|
|
634
|
-
notCarried = `the approval was not carried: ${carried.reason}`;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
let reviewedHere = false;
|
|
640
|
-
if (unreviewed !== undefined) {
|
|
641
|
-
const files = unreviewed;
|
|
642
|
-
const head = pipeline.head;
|
|
643
|
-
const reviewed = yield* call(`review:${cycle}`, reviewLoop(), {
|
|
644
|
-
base: `origin/${target}`,
|
|
645
|
-
head,
|
|
646
|
-
files: files.files,
|
|
647
|
-
...head === files.synced ? undefined : { since: files.synced },
|
|
648
|
-
...files.earlier.length === 0 ? undefined : { earlier: files.earlier }
|
|
649
|
-
});
|
|
650
|
-
if (reviewed.blocking) {
|
|
651
|
-
const blocking = reviewed.findings.filter(isBlocking);
|
|
652
|
-
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
653
|
-
return done("needs_you", `The review of the catch-up merge's files still finds ${blocking.map((finding) => finding.id).join(", ")} after ${fixRounds} fix rounds`);
|
|
654
|
-
fixRounds++;
|
|
655
|
-
yield* agentWork(`review-fix:${cycle}`, {
|
|
656
|
-
agent: "implementer",
|
|
657
|
-
prompt: reviewFixPrompt(blocking),
|
|
658
|
-
output: ReviewFix,
|
|
659
|
-
timeout: stepBudget
|
|
660
|
-
});
|
|
661
|
-
unreviewed = { ...files, earlier: reviewed.findings };
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
const approval = yield* approveChangeRequest(`approve:${cycle}`, {
|
|
665
|
-
changeRequestId,
|
|
666
|
-
head,
|
|
667
|
-
summary: `A review of the files merging ${target} in changed (${files.files.join(", ")}) passed with nothing blocking.`,
|
|
668
|
-
review: `review:${cycle}`,
|
|
669
|
-
carries: files.sync
|
|
670
|
-
}).pipe(catchTag("StepFailed", (error) => succeed6({ approved: false, reason: error.reason })));
|
|
671
|
-
unreviewed = undefined;
|
|
672
|
-
if (approval.approved)
|
|
673
|
-
reviewedHere = true;
|
|
674
|
-
else
|
|
675
|
-
notCarried = `merging ${target} in, whose files' review passed but did not approve it: ${approval.reason}`;
|
|
676
|
-
}
|
|
677
|
-
const moved = pipeline.readAt === undefined ? [] : [
|
|
678
|
-
{ event: "change_request.pipeline", filter: { changeRequestId }, since: pipeline.readAt },
|
|
679
|
-
...pipeline.source === "reported" ? [
|
|
680
|
-
{
|
|
681
|
-
event: "change_request.approval",
|
|
682
|
-
filter: { changeRequestId, approved: true },
|
|
683
|
-
since: pipeline.readAt
|
|
684
|
-
}
|
|
685
|
-
] : []
|
|
686
|
-
];
|
|
687
|
-
const approved = reviewedHere ? undefined : yield* waitFor(`approved:${cycle}`, {
|
|
688
|
-
event: "change_request.approval",
|
|
689
|
-
filter: { changeRequestId, approved: true, head: pipeline.head },
|
|
690
|
-
timeout: "7 days",
|
|
691
|
-
or: [commented, ...moved]
|
|
692
|
-
});
|
|
693
|
-
if (approved !== undefined && approved.type !== "change_request.approval")
|
|
694
|
-
continue;
|
|
695
|
-
if (pipeline.source === "reported" && approved !== undefined && approved.payload.head !== pipeline.head)
|
|
696
|
-
continue;
|
|
697
|
-
const merge = input.merge === true ? "Merge" : yield* ask(`merge:${cycle}`, {
|
|
698
|
-
prompt: "The ChangeRequest is approved, green and mergeable. Merge now?",
|
|
699
|
-
options: [
|
|
700
|
-
{ value: "Merge", description: "Merges the head whose pipeline passed." },
|
|
701
|
-
{ value: "Wait", description: "Leaves the merge to a person." }
|
|
702
|
-
],
|
|
703
|
-
recommended: "Merge",
|
|
704
|
-
recommendationReason: "It is approved, green and mergeable, which is everything a merge waits for.",
|
|
705
|
-
default: "Wait",
|
|
706
|
-
timeout: "1 hour"
|
|
707
|
-
});
|
|
708
|
-
if (merge === "Wait")
|
|
709
|
-
return done("ready", "Approved, green and mergeable; left for a person to merge");
|
|
710
|
-
const merged = yield* mergeChangeRequest(`merge-change-request:${cycle}`, {
|
|
711
|
-
changeRequestId,
|
|
712
|
-
head: pipeline.head
|
|
713
|
-
});
|
|
714
|
-
if (merged.status !== "merged")
|
|
715
|
-
continue;
|
|
716
|
-
yield* writeBack("write-back", { milestone: "merged" });
|
|
717
|
-
return done("merged", `Merged at ${merged.revision}`);
|
|
718
|
-
}
|
|
719
|
-
}).pipe(catchIf((error) => error._tag === "StepFailed" && error.reason.includes("time budget"), (error) => succeed6(done("needs_you", `Step "${error.step}" ran past its ${stepBudget} budget`))), catchTags({
|
|
720
|
-
WaitTimedOut: (timedOut) => succeed6(done("needs_you", timedOut.event === "change_request.approval" && notCarried !== undefined ? `Nobody approved the head after ${notCarried}` : `Nothing happened on the ChangeRequest while waiting for ${timedOut.event}`)),
|
|
721
|
-
ChangeRequestClosed: (ended) => succeed6(ended.merged ? done("merged", "Someone else merged the ChangeRequest") : done("needs_you", "The ChangeRequest was closed"))
|
|
722
|
-
}));
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
// packages/loops/src/build.ts
|
|
727
|
-
var BuildInput = Struct({
|
|
728
|
-
words: optional(String4),
|
|
729
|
-
taskId: optional(String4),
|
|
730
|
-
repoBindingId: optional(String4),
|
|
731
|
-
branch: optional(String4),
|
|
732
|
-
revision: optional(String4),
|
|
733
|
-
outcome: optional(LoopOutcome),
|
|
734
|
-
maxRounds: optional(Int),
|
|
735
|
-
continuing: optional(Continuing),
|
|
736
|
-
land: optional(Boolean2),
|
|
737
|
-
changeRequestId: optional(String4)
|
|
738
|
-
});
|
|
739
|
-
var CriterionAnswer = Struct({
|
|
740
|
-
criterion: String4,
|
|
741
|
-
status: Literals(["met", "after_merge", "not_met"]),
|
|
742
|
-
detail: String4
|
|
743
|
-
});
|
|
744
|
-
var Criteria = Struct({ criteria: ArraySchema(CriterionAnswer) });
|
|
745
|
-
var SuggestedCheck = Struct({
|
|
746
|
-
name: CheckName,
|
|
747
|
-
command: String4,
|
|
748
|
-
workingDirectory: optionalKey(String4)
|
|
749
|
-
});
|
|
750
|
-
var Implemented = Struct({
|
|
751
|
-
title: String4,
|
|
752
|
-
summary: String4,
|
|
753
|
-
claims: ArraySchema(String4),
|
|
754
|
-
documented: optional(ArraySchema(String4)),
|
|
755
|
-
rollback: optional(String4),
|
|
756
|
-
criteria: optional(ArraySchema(CriterionAnswer)),
|
|
757
|
-
checks: optional(ArraySchema(SuggestedCheck))
|
|
758
|
-
});
|
|
759
|
-
var Reproduced = Struct({
|
|
760
|
-
check: String4,
|
|
761
|
-
summary: String4
|
|
762
|
-
});
|
|
763
|
-
var ReproducedOnBranch = Struct({
|
|
764
|
-
...Reproduced.fields,
|
|
765
|
-
revision: optional(String4)
|
|
766
|
-
});
|
|
767
|
-
var Fixed = Struct({
|
|
768
|
-
summary: String4,
|
|
769
|
-
claims: ArraySchema(String4),
|
|
770
|
-
rebuttals: ArraySchema(Rebuttal)
|
|
771
|
-
});
|
|
772
|
-
var BuildOutput = Struct({
|
|
773
|
-
changeRequest: ChangeRequestResult,
|
|
774
|
-
revision: String4,
|
|
775
|
-
tree: String4,
|
|
776
|
-
settled: Boolean2,
|
|
777
|
-
unsettled: ArraySchema(String4),
|
|
778
|
-
rounds: Int,
|
|
779
|
-
stopped: Literals(["settled", "dispute_stands", "findings_returned", "round_cap"]),
|
|
780
|
-
criteria: ArraySchema(CriterionAnswer),
|
|
781
|
-
approval: optional(ApproveChangeRequestResult),
|
|
782
|
-
landed: optional(ShepherdOutput),
|
|
783
|
-
next: Struct({ changeRequestId: String4 })
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
class BuildTargetMissing extends TaggedError2()("BuildTargetMissing", {
|
|
787
|
-
message: String4
|
|
788
|
-
}) {
|
|
789
|
-
}
|
|
790
|
-
var outcomeGuidance = {
|
|
791
|
-
feature: "Deliver the whole scope; review checks it was met.",
|
|
792
|
-
bug: "Your failing test is already in place: make it pass.",
|
|
793
|
-
refactor: "Keep behaviour exactly as it is; the approved verifications must pass before and after.",
|
|
794
|
-
investigation: "Investigate and report what you found, with its sources.",
|
|
795
|
-
docs: "Name, in `documented`, the approved checks that run the commands you documented.",
|
|
796
|
-
migration: "Name, in `rollback`, the approved check that rolls the migration back.",
|
|
797
|
-
performance: "Make it faster without changing behaviour."
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
class BuildOutcomeRefused extends TaggedError2()("BuildOutcomeRefused", {
|
|
801
|
-
message: String4
|
|
802
|
-
}) {
|
|
803
|
-
}
|
|
804
|
-
var criteriaGuidance = "If your brief has a Task, answer each of its acceptance criteria in `criteria`: met, with the test or file that shows it; after_merge, with why it can only be seen once merged; or not_met, with why. Give an empty `criteria` when the Task has none.";
|
|
805
|
-
var checksGuidance = "If your brief lists no approved verifications for this repository, name in `checks` the commands from its package scripts or CI config that test, lint and typecheck it, each with a `name` of letters, digits, `.`, `_`, `:` and `-` only, such as `unit-tests`; Kitman runs them and labels them as suggested by you.";
|
|
806
|
-
var implementAdaptation = "You run the implement skill as a Kitman Step. The work is in this prompt and your brief, so fetch no ticket. Leave your changes uncommitted: the Run owns the branch. Skip its final code-review: a Review run or a person reviews the change.";
|
|
807
|
-
var implementPrompt = (task, outcome, continuing) => [
|
|
808
|
-
...continuing === undefined ? [] : [continuingBrief(continuing), "Fix forward from there."],
|
|
809
|
-
`Build ${task}`,
|
|
810
|
-
implementAdaptation,
|
|
811
|
-
...outcome === undefined ? [] : [outcomeGuidance[outcome]],
|
|
812
|
-
'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".',
|
|
813
|
-
criteriaGuidance,
|
|
814
|
-
checksGuidance
|
|
815
|
-
].join(`
|
|
816
|
-
|
|
817
|
-
`);
|
|
818
|
-
var criteriaPrompt = (unanswered, unmet) => [
|
|
819
|
-
unanswered ? "You did not answer the Task's acceptance criteria. Answer each one before anyone reviews the change." : "These acceptance criteria are not met yet. Meet them, or say why they cannot be met here:",
|
|
820
|
-
...unmet.map((answer) => `- ${answer.criterion}: ${answer.detail}`),
|
|
821
|
-
criteriaGuidance
|
|
822
|
-
].join(`
|
|
823
|
-
|
|
824
|
-
`);
|
|
825
|
-
var answerLine = (answer) => `${answer.criterion}: ${answer.status.replaceAll("_", " ")}, ${answer.detail}`;
|
|
826
|
-
var reproducePrompt = (task) => [
|
|
827
|
-
`Reproduce the bug in ${task} before anyone fixes it.`,
|
|
828
|
-
"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."
|
|
829
|
-
].join(`
|
|
830
|
-
|
|
831
|
-
`);
|
|
832
|
-
var reproducedOnBranchPrompt = (task) => [
|
|
833
|
-
`An earlier Run already worked on fixing the bug in ${task}, on this branch.`,
|
|
834
|
-
"Find the commit on this branch where the test reproducing the bug was in place and the fix was not yet, and give it as `revision`; name the approved check that runs that test. Change nothing.",
|
|
835
|
-
"If no commit on the branch has the reproducing test without the fix, add a test that fails on the branch head now, change nothing else, and give no `revision`."
|
|
836
|
-
].join(`
|
|
837
|
-
|
|
838
|
-
`);
|
|
839
|
-
var findingLine = (finding) => `- **${finding.severity}** \`${finding.id}\` ${finding.detail}`;
|
|
840
|
-
var disputeLine = ({ finding, rebuttal }) => `${findingLine(finding)}
|
|
841
|
-
You rebutted: ${rebuttal.reason}`;
|
|
842
|
-
var fixPrompt = (settled) => {
|
|
843
|
-
const checks = settled.open.filter((finding) => isCheckFinding(finding.id));
|
|
844
|
-
const findings = settled.open.filter((finding) => !isCheckFinding(finding.id));
|
|
845
|
-
return [
|
|
846
|
-
...checks.length === 0 ? [] : [
|
|
847
|
-
"These checks fail on your head and pass on the base. Only the check passing closes one. If you are sure the change did not cause it, such as a flaky test, rebut it with your reason: the ChangeRequest reports that as your claim and the check stays open. A check does not run again on an unchanged tree, so if you change nothing and only these are open, the rally ends.",
|
|
848
|
-
...checks.map(findingLine)
|
|
849
|
-
],
|
|
850
|
-
...findings.length === 0 ? [] : [
|
|
851
|
-
"The review found these. Fix each, or rebut it with your reason if it is wrong; reviewers judge the rebuttals next round.",
|
|
852
|
-
...findings.map(findingLine)
|
|
853
|
-
],
|
|
854
|
-
...settled.standing.length === 0 ? [] : [
|
|
855
|
-
"The reviewers rejected your rebuttal of these. Fix each, or rebut it again with a better reason; one you leave stays a dispute for a person to settle.",
|
|
856
|
-
...settled.standing.map(disputeLine)
|
|
857
|
-
]
|
|
858
|
-
].join(`
|
|
859
|
-
`);
|
|
860
|
-
};
|
|
861
|
-
var blockingIds = (settled) => settled.open.filter(isBlocking).map((finding) => finding.id);
|
|
862
|
-
var onlyChecksBlock = (settled) => blockingIds(settled).every(isCheckFinding) && !settled.standing.some((entry) => isBlocking(entry.finding));
|
|
863
|
-
var rally = (input) => gen(function* () {
|
|
864
|
-
const claims = [...input.claims];
|
|
865
|
-
const conflicts = [];
|
|
866
|
-
let rebuttals = [];
|
|
867
|
-
let earlier = [];
|
|
868
|
-
let previous;
|
|
869
|
-
let last;
|
|
870
|
-
const shown = new Set;
|
|
871
|
-
for (let round = 1;; round++) {
|
|
872
|
-
const synced = yield* syncBase(`sync:${round}`, {
|
|
873
|
-
agent: "implementer",
|
|
874
|
-
...input.target === undefined ? undefined : { target: input.target }
|
|
875
|
-
});
|
|
876
|
-
if (synced.status === "conflict")
|
|
877
|
-
conflicts.push(`Could not merge the target branch in round ${round}: ${synced.files.join(", ")} (${synced.reason})`);
|
|
878
|
-
const head = yield* verify(`verify:${round}`, { suggested: input.suggested });
|
|
879
|
-
if (last !== undefined && last.head.tree === head.tree && rebuttals.length === 0 && onlyChecksBlock(last.settled))
|
|
880
|
-
return {
|
|
881
|
-
...last,
|
|
882
|
-
round: round - 1,
|
|
883
|
-
head,
|
|
884
|
-
decision: { stop: true, reason: "findings_returned", unsettled: blockingIds(last.settled) },
|
|
885
|
-
claims,
|
|
886
|
-
conflicts
|
|
887
|
-
};
|
|
888
|
-
const failing = brokenChecks(head).map((check) => check.name);
|
|
889
|
-
const reviewed = yield* call(`review:${round}`, review_default, {
|
|
890
|
-
base: input.base,
|
|
891
|
-
head: head.revision,
|
|
892
|
-
...rebuttals.length === 0 ? undefined : { rebuttals },
|
|
893
|
-
...earlier.length === 0 ? undefined : { earlier },
|
|
894
|
-
...input.outcome === undefined ? undefined : { outcome: input.outcome },
|
|
895
|
-
...failing.length === 0 ? undefined : { failing },
|
|
896
|
-
claims: [...claims]
|
|
897
|
-
});
|
|
898
|
-
const checks = checkFindings(head);
|
|
899
|
-
const settled = settleRound({
|
|
900
|
-
findings: [...reviewed.findings, ...scopeFindings(reviewed.scope), ...checks],
|
|
901
|
-
rebuttals,
|
|
902
|
-
verdicts: reviewed.verdicts
|
|
903
|
-
});
|
|
904
|
-
earlier = [...settled.open, ...settled.standing.map((entry) => entry.finding)].filter((finding) => !finding.id.startsWith("scope: ") && !isCheckFinding(finding.id));
|
|
905
|
-
const unseen = checks.some((finding) => !shown.has(finding.detail));
|
|
906
|
-
const decision = rallyStop({
|
|
907
|
-
round,
|
|
908
|
-
maxRounds: input.maxRounds,
|
|
909
|
-
settled,
|
|
910
|
-
...previous === undefined || unseen ? undefined : { previous }
|
|
911
|
-
});
|
|
912
|
-
if (decision.stop && (!unseen || round >= input.maxRounds))
|
|
913
|
-
return { round, head, settled, decision, claims, scope: reviewed.scope, conflicts };
|
|
914
|
-
for (const finding of checks)
|
|
915
|
-
shown.add(finding.detail);
|
|
916
|
-
const fixed = yield* agentWork(`fix:${round}`, {
|
|
917
|
-
agent: "implementer",
|
|
918
|
-
prompt: fixPrompt(settled),
|
|
919
|
-
output: Fixed
|
|
920
|
-
});
|
|
921
|
-
claims.push(...fixed.claims, ...fixed.rebuttals.flatMap((rebuttal) => isCheckFinding(rebuttal.findingId) ? [`Rebutted ${rebuttal.findingId}: ${rebuttal.reason}`] : []));
|
|
922
|
-
const rebutted = fixed.rebuttals.filter((rebuttal) => !isCheckFinding(rebuttal.findingId));
|
|
923
|
-
rebuttals = [
|
|
924
|
-
...rebutted,
|
|
925
|
-
...settled.standing.filter((entry) => isBlocking(entry.finding)).map((entry) => entry.rebuttal).filter((rebuttal) => !rebutted.some((entry) => entry.findingId === rebuttal.findingId))
|
|
926
|
-
];
|
|
927
|
-
previous = blockingIds(settled);
|
|
928
|
-
last = { head, settled, scope: reviewed.scope };
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
var build_default = defineLoop({
|
|
932
|
-
id: "build",
|
|
933
|
-
version: "2.2.0",
|
|
934
|
-
title: "Build",
|
|
935
|
-
input: BuildInput,
|
|
936
|
-
output: BuildOutput,
|
|
937
|
-
agents: { implementer: "implementer" },
|
|
938
|
-
checkout: "write",
|
|
939
|
-
offers: [
|
|
940
|
-
{ id: "shepherd", title: "Shepherd it to merge", loop: "shepherd", from: "next" },
|
|
941
|
-
{ id: "review", title: "Review again", loop: "review", from: "next" },
|
|
942
|
-
{ id: "tidy", title: "Tidy", loop: "tidy" },
|
|
943
|
-
{ id: "retry", title: "Retry with a steer", loop: "build" }
|
|
944
|
-
],
|
|
945
|
-
capabilities: ["agent_work", "verify", "change_request", "write_back", "post_comment", "approve_change_request"],
|
|
946
|
-
calls: () => [review_default, shepherd_default],
|
|
947
|
-
run: (input) => gen(function* () {
|
|
948
|
-
if (input.words === undefined && input.taskId === undefined)
|
|
949
|
-
return yield* new BuildTargetMissing({ message: "Give the words to build, or a Task" });
|
|
950
|
-
if (input.outcome === "performance")
|
|
951
|
-
return yield* new BuildOutcomeRefused({
|
|
952
|
-
message: "Build cannot prove a performance gain; start Hillclimb, which commits a benchmark and keeps only the changes Kitman measures faster"
|
|
953
|
-
});
|
|
954
|
-
yield* writeBack("write-back:started", { milestone: "started" });
|
|
955
|
-
const task = input.words === undefined ? "the Task your brief describes." : `this: ${input.words}`;
|
|
956
|
-
const outcome = input.outcome;
|
|
957
|
-
const target = input.branch ?? input.continuing?.targetBranch;
|
|
958
|
-
if (input.changeRequestId !== undefined && input.continuing?.changeRequestId !== input.changeRequestId)
|
|
959
|
-
return yield* new BuildTargetMissing({
|
|
960
|
-
message: `Kitman did not put this Run on ChangeRequest ${input.changeRequestId}'s branch`
|
|
961
|
-
});
|
|
962
|
-
const reproduced = outcome === "bug" ? yield* agentWork("reproduce", {
|
|
963
|
-
agent: "implementer",
|
|
964
|
-
prompt: input.continuing === undefined ? reproducePrompt(task) : reproducedOnBranchPrompt(task),
|
|
965
|
-
output: ReproducedOnBranch
|
|
966
|
-
}) : undefined;
|
|
967
|
-
const base = reproduced !== undefined ? yield* verify("verify:reproduction", reproduced.revision === undefined ? {} : { revision: reproduced.revision }) : outcome === "refactor" ? yield* verify("verify:base", {}) : undefined;
|
|
968
|
-
const implemented = yield* agentWorkOnce("implement", {
|
|
969
|
-
agent: "implementer",
|
|
970
|
-
skill: "implement",
|
|
971
|
-
prompt: implementPrompt(task, outcome, input.continuing),
|
|
972
|
-
output: Implemented
|
|
973
|
-
});
|
|
974
|
-
const unanswered = input.taskId !== undefined && implemented.criteria === undefined;
|
|
975
|
-
const unmet = (implemented.criteria ?? []).filter((answer) => answer.status === "not_met");
|
|
976
|
-
const criteria = unanswered || unmet.length > 0 ? (yield* agentWorkOnce("criteria", {
|
|
977
|
-
agent: "implementer",
|
|
978
|
-
prompt: criteriaPrompt(unanswered, unmet),
|
|
979
|
-
output: Criteria
|
|
980
|
-
})).criteria : implemented.criteria ?? [];
|
|
981
|
-
const last = yield* rally({
|
|
982
|
-
maxRounds: input.maxRounds ?? 3,
|
|
983
|
-
base: `origin/${target ?? "HEAD"}`,
|
|
984
|
-
target,
|
|
985
|
-
outcome,
|
|
986
|
-
claims: [...implemented.claims, ...criteria.map((answer) => `Criterion ${answerLine(answer)}`)],
|
|
987
|
-
suggested: implemented.checks ?? []
|
|
988
|
-
});
|
|
989
|
-
const verdict = evidenceGate({
|
|
990
|
-
...outcome === undefined ? undefined : { outcome },
|
|
991
|
-
head: last.head,
|
|
992
|
-
...base === undefined ? undefined : { base },
|
|
993
|
-
...last.scope === undefined ? undefined : { review: last.scope },
|
|
994
|
-
...reproduced === undefined ? undefined : { reproducing: reproduced.check },
|
|
995
|
-
...implemented.documented === undefined ? undefined : { documented: implemented.documented },
|
|
996
|
-
...implemented.rollback === undefined ? undefined : { rollback: implemented.rollback }
|
|
997
|
-
});
|
|
998
|
-
const unsettled = [
|
|
999
|
-
...new Set([
|
|
1000
|
-
...verdict.settled ? [] : verdict.reasons,
|
|
1001
|
-
...brokenChecks(last.head).map((check) => failsOn("head", check.name, last.head.tree)),
|
|
1002
|
-
...unsettledItems(last.settled, last.decision),
|
|
1003
|
-
...last.conflicts,
|
|
1004
|
-
...criteria.flatMap((answer) => answer.status === "not_met" ? [`Criterion not met: ${answer.criterion} (${answer.detail})`] : [])
|
|
1005
|
-
])
|
|
1006
|
-
];
|
|
1007
|
-
const body = changeRequestBody({
|
|
1008
|
-
summary: implemented.summary,
|
|
1009
|
-
head: last.head,
|
|
1010
|
-
claims: last.claims,
|
|
1011
|
-
unsettled,
|
|
1012
|
-
criteria: criteria.map(answerLine),
|
|
1013
|
-
later: last.settled.open.filter((finding) => !isBlocking(finding)).map((finding) => `${finding.id}: ${finding.detail}`),
|
|
1014
|
-
rounds: last.round,
|
|
1015
|
-
stopped: last.decision.reason
|
|
1016
|
-
});
|
|
1017
|
-
const named = input.changeRequestId === undefined ? undefined : input.continuing;
|
|
1018
|
-
const opened = named === undefined ? yield* changeRequest("change-request", { title: implemented.title, body }) : yield* comment("report", { changeRequestId: named.changeRequestId, body, summary: "kitman-build" }).pipe(as({ id: named.changeRequestId, number: named.number, url: named.url }));
|
|
1019
|
-
if (named === undefined)
|
|
1020
|
-
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
1021
|
-
const approval = unsettled.length === 0 ? yield* approveChangeRequest("approve", {
|
|
1022
|
-
changeRequestId: opened.id,
|
|
1023
|
-
head: last.head.revision,
|
|
1024
|
-
summary: `Review round ${last.round} passed with nothing blocking.`,
|
|
1025
|
-
review: `review:${last.round}`
|
|
1026
|
-
}).pipe(catchTag("StepFailed", (error) => succeed6({ approved: false, reason: error.reason }))) : undefined;
|
|
1027
|
-
const landed = input.land === true ? yield* call("land", shepherd_default, {
|
|
1028
|
-
changeRequestId: opened.id,
|
|
1029
|
-
...input.taskId === undefined ? undefined : { taskId: input.taskId },
|
|
1030
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
1031
|
-
}) : undefined;
|
|
1032
|
-
return {
|
|
1033
|
-
changeRequest: opened,
|
|
1034
|
-
revision: last.head.revision,
|
|
1035
|
-
tree: last.head.tree,
|
|
1036
|
-
settled: unsettled.length === 0,
|
|
1037
|
-
unsettled,
|
|
1038
|
-
rounds: last.round,
|
|
1039
|
-
stopped: last.decision.reason,
|
|
1040
|
-
criteria,
|
|
1041
|
-
...approval === undefined ? undefined : { approval },
|
|
1042
|
-
...landed === undefined ? undefined : { landed },
|
|
1043
|
-
next: { changeRequestId: opened.id }
|
|
1044
|
-
};
|
|
1045
|
-
}).pipe(tapError2(() => ignore(writeBack("write-back:failed", { milestone: "failed" }))))
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
// packages/loops/src/errand.ts
|
|
1049
|
-
var Input3 = Struct({
|
|
1050
|
-
words: String4,
|
|
1051
|
-
taskId: optional(String4),
|
|
1052
|
-
repoBindingId: optional(String4),
|
|
1053
|
-
branch: optional(String4)
|
|
1054
|
-
});
|
|
1055
|
-
var Done = Struct({
|
|
1056
|
-
status: Literals(["done", "blocked", "too_big"]),
|
|
1057
|
-
summary: String4,
|
|
1058
|
-
changed: Boolean2,
|
|
1059
|
-
title: optional(String4),
|
|
1060
|
-
checks: optional(ArraySchema(SuggestedCheck))
|
|
1061
|
-
});
|
|
1062
|
-
var budget = "1 hour";
|
|
1063
|
-
var Output3 = Struct({
|
|
1064
|
-
status: Literals(["done", "blocked", "too_big", "opened", "handed_to_build", "discarded"]),
|
|
1065
|
-
summary: String4,
|
|
1066
|
-
changeRequest: optional(ChangeRequestResult)
|
|
1067
|
-
});
|
|
1068
|
-
var prompt = (words) => [
|
|
1069
|
-
`Just do this: ${words}`,
|
|
1070
|
-
implementAdaptation,
|
|
1071
|
-
"It is an errand: a rename, a bump, regenerated fixtures, an answer to a question. Do it directly, run the tests of what you touch, and commit nothing you were not asked for.",
|
|
1072
|
-
"If it turns out to be a feature (several parts, design choices, or more than an hour of work), change nothing and answer too_big with why: a Build will do it with a full review. If something stops you, answer blocked with what.",
|
|
1073
|
-
"Answer with a summary for the person who asked, whether you changed files, a ChangeRequest title if you did, and checks to run if the repository has none approved."
|
|
1074
|
-
].join(`
|
|
1075
|
-
|
|
1076
|
-
`);
|
|
1077
|
-
var errand_default = defineLoop({
|
|
1078
|
-
id: "errand",
|
|
1079
|
-
version: "1.0.0",
|
|
1080
|
-
title: "Errand",
|
|
1081
|
-
input: Input3,
|
|
1082
|
-
output: Output3,
|
|
1083
|
-
agents: { implementer: "implementer" },
|
|
1084
|
-
checkout: "write",
|
|
1085
|
-
offers: [{ id: "build", title: "Build it with a full review", loop: "build" }],
|
|
1086
|
-
capabilities: ["agent_work", "verify", "change_request", "write_back", "ask"],
|
|
1087
|
-
calls: () => [build_default],
|
|
1088
|
-
run: (input) => gen(function* () {
|
|
1089
|
-
const done = yield* agentWorkOnce("errand", {
|
|
1090
|
-
agent: "implementer",
|
|
1091
|
-
skill: "implement",
|
|
1092
|
-
prompt: prompt(input.words),
|
|
1093
|
-
output: Done,
|
|
1094
|
-
timeout: budget
|
|
1095
|
-
}).pipe(catchIf((error) => error._tag === "StepFailed" && error.reason.includes("time budget"), () => succeed6({
|
|
1096
|
-
status: "too_big",
|
|
1097
|
-
summary: `It took longer than an errand's ${budget}, so it is a feature: Build can do it with a full review.`,
|
|
1098
|
-
changed: false
|
|
1099
|
-
})));
|
|
1100
|
-
if (done.status !== "done" || !done.changed)
|
|
1101
|
-
return { status: done.status, summary: done.summary };
|
|
1102
|
-
const head = yield* verify("verify", { suggested: done.checks ?? [] });
|
|
1103
|
-
const answer = yield* ask("next", {
|
|
1104
|
-
prompt: `${done.summary}
|
|
1105
|
-
|
|
1106
|
-
The checks ${head.passed ? "pass" : "fail"}. What next?`,
|
|
1107
|
-
options: [
|
|
1108
|
-
{ value: "Open a ChangeRequest", description: "Deliver it as it is, unreviewed." },
|
|
1109
|
-
{ value: "Hand to Build for a full review", description: "Build reviews and verifies it before delivering." },
|
|
1110
|
-
{ value: "Discard", description: "Drop the change." }
|
|
1111
|
-
],
|
|
1112
|
-
recommended: head.passed ? "Open a ChangeRequest" : "Hand to Build for a full review",
|
|
1113
|
-
recommendationReason: head.passed ? "The checks pass, and an errand is small enough to review on the ChangeRequest." : "The checks fail, so it needs the fixes and review a Build gives before anyone merges it.",
|
|
1114
|
-
default: "Open a ChangeRequest",
|
|
1115
|
-
timeout: "1 hour"
|
|
1116
|
-
});
|
|
1117
|
-
if (answer === "Discard")
|
|
1118
|
-
return { status: "discarded", summary: done.summary };
|
|
1119
|
-
if (answer === "Hand to Build for a full review") {
|
|
1120
|
-
const built = yield* call("build", build_default, {
|
|
1121
|
-
words: input.words,
|
|
1122
|
-
revision: head.revision,
|
|
1123
|
-
...input.taskId === undefined ? undefined : { taskId: input.taskId },
|
|
1124
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
1125
|
-
});
|
|
1126
|
-
return { status: "handed_to_build", summary: done.summary, changeRequest: built.changeRequest };
|
|
1127
|
-
}
|
|
1128
|
-
const opened = yield* changeRequest("change-request", {
|
|
1129
|
-
title: done.title ?? input.words.slice(0, 120),
|
|
1130
|
-
body: [
|
|
1131
|
-
done.summary,
|
|
1132
|
-
"> **Unreviewed errand.** One agent did this with no review; ask for a Build or a Review if it needs one.",
|
|
1133
|
-
"## Evidence",
|
|
1134
|
-
evidenceTable(head)
|
|
1135
|
-
].join(`
|
|
1136
|
-
|
|
1137
|
-
`)
|
|
1138
|
-
});
|
|
1139
|
-
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
1140
|
-
return { status: "opened", summary: done.summary, changeRequest: opened };
|
|
1141
|
-
})
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
// packages/loops/src/file-tasks.ts
|
|
1145
|
-
var FileTasksInput = Struct({
|
|
1146
|
-
tickets: PlanTickets,
|
|
1147
|
-
projectId: optional(String4)
|
|
1148
|
-
});
|
|
1149
|
-
var Output4 = Struct({
|
|
1150
|
-
tasks: ArraySchema(Struct({ key: String4, taskId: String4 }))
|
|
1151
|
-
});
|
|
1152
|
-
var file_tasks_default = defineLoop({
|
|
1153
|
-
id: "file-tasks",
|
|
1154
|
-
version: "1.0.0",
|
|
1155
|
-
title: "File Tasks",
|
|
1156
|
-
input: FileTasksInput,
|
|
1157
|
-
output: Output4,
|
|
1158
|
-
agents: {},
|
|
1159
|
-
checkout: "none",
|
|
1160
|
-
capabilities: ["create_tasks"],
|
|
1161
|
-
run: (input) => createTasks("tasks", {
|
|
1162
|
-
tickets: input.tickets,
|
|
1163
|
-
...input.projectId === undefined ? undefined : { projectId: input.projectId }
|
|
1164
|
-
}).pipe(map8(({ tasks }) => ({ tasks })))
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
// packages/loops/src/hillclimb.ts
|
|
1168
|
-
var Input4 = Struct({
|
|
1169
|
-
focus: optional(String4),
|
|
1170
|
-
taskId: optional(String4),
|
|
1171
|
-
repoBindingId: optional(String4),
|
|
1172
|
-
branch: optional(String4),
|
|
1173
|
-
cycles: optional(Int),
|
|
1174
|
-
threshold: optional(Finite),
|
|
1175
|
-
tolerance: optional(Finite)
|
|
1176
|
-
});
|
|
1177
|
-
var Opportunity = Struct({
|
|
1178
|
-
title: String4,
|
|
1179
|
-
detail: String4,
|
|
1180
|
-
evidence: Literals(["measured", "telemetry", "hypothesis"]),
|
|
1181
|
-
external: optional(Boolean2)
|
|
1182
|
-
});
|
|
1183
|
-
var Discovery = Struct({
|
|
1184
|
-
stack: String4,
|
|
1185
|
-
journeys: ArraySchema(Struct({ name: String4, objective: String4 })),
|
|
1186
|
-
telemetry: ArraySchema(Struct({ source: String4, status: Literals(["used", "unavailable"]), why: String4 })),
|
|
1187
|
-
opportunities: ArraySchema(Opportunity)
|
|
1188
|
-
});
|
|
1189
|
-
var Harness = Struct({
|
|
1190
|
-
summary: String4,
|
|
1191
|
-
plan: MeasurePlan
|
|
1192
|
-
});
|
|
1193
|
-
var Change = Struct({
|
|
1194
|
-
status: Literals(["changed", "exhausted", "unsafe"]),
|
|
1195
|
-
summary: String4
|
|
1196
|
-
});
|
|
1197
|
-
var Kept = Struct({ summary: String4, verdict: String4, reason: String4 });
|
|
1198
|
-
var Output5 = Struct({
|
|
1199
|
-
status: Literals(["improved", "no_gain", "gain_gone", "checks_failed", "too_noisy"]),
|
|
1200
|
-
reason: String4,
|
|
1201
|
-
kept: ArraySchema(Kept),
|
|
1202
|
-
cycles: Int,
|
|
1203
|
-
reportArtifactId: String4,
|
|
1204
|
-
changeRequest: optional(ChangeRequestResult),
|
|
1205
|
-
external: ArraySchema(String4),
|
|
1206
|
-
unsettled: optional(ArraySchema(String4))
|
|
1207
|
-
});
|
|
1208
|
-
var discoverPrompt = (focus) => [
|
|
1209
|
-
`Find where this repository is slow${focus === undefined ? ", for the journeys that matter most" : `: ${focus}`}. Change nothing.`,
|
|
1210
|
-
"Read the stack, the user journeys and the objective each has, the docs and earlier performance work. Read whatever telemetry you can reach, read-only and with bounded queries; list each source as used or unavailable, with why.",
|
|
1211
|
-
"Rank the opportunities, most promising first, each marked measured, telemetry or hypothesis. Mark one that only another repository can make as external."
|
|
1212
|
-
].join(`
|
|
1213
|
-
|
|
1214
|
-
`);
|
|
1215
|
-
var harnessPrompt = (discovery) => [
|
|
1216
|
-
"Commit a local benchmark for these journeys, and change no product code:",
|
|
1217
|
-
...discovery.journeys.map((journey) => `- ${journey.name}: ${journey.objective}`),
|
|
1218
|
-
"It runs on this Runner alone: never call a service, database or URL outside it. Each run prints one JSON line of numbers by metric name as its last line; wall time is measured for you as wall_ms. Keep it under a minute a run.",
|
|
1219
|
-
"Give the measurement plan: the command, any prepare command, and the metrics with one primary and the rest guardrails. Add a low-variance counter (instructions, allocations, queries) where you can, marked lowVariance."
|
|
1220
|
-
].join(`
|
|
1221
|
-
|
|
1222
|
-
`);
|
|
1223
|
-
var changePrompt = (discovery, plan, tried, cycle) => [
|
|
1224
|
-
`Cycle ${cycle}: make one bounded change that should improve ${plan.metrics.find((metric) => metric.role === "primary")?.name ?? "the primary metric"} without making a guardrail worse, and commit it. Kitman measures it; do not report numbers of your own.`,
|
|
1225
|
-
"Opportunities, most promising first:",
|
|
1226
|
-
...discovery.opportunities.flatMap((entry) => entry.external === true ? [] : [`- ${entry.title} (${entry.evidence}): ${entry.detail}`]),
|
|
1227
|
-
...tried.length === 0 ? [] : ["Already tried:", ...tried.map((entry) => `- ${entry}`)],
|
|
1228
|
-
"Keep behaviour exactly as it is and run the tests you touch. Answer exhausted when nothing is left worth trying, or unsafe when the gain would cost correctness; either way change nothing."
|
|
1229
|
-
].join(`
|
|
1230
|
-
|
|
1231
|
-
`);
|
|
1232
|
-
var revertPrompt = (start, cycle) => `Cycle ${cycle}'s change did not prove faster. Revert its commits, and only those: run \`git revert --no-edit ${start}..HEAD\`. Touch no commit at or before ${start}, never reset or force-push, and change nothing else.`;
|
|
1233
|
-
var fixPrompt2 = (findings) => [
|
|
1234
|
-
"The review of the climb found these. Fix each, keep behaviour exactly as it is and keep the gain, and commit. Kitman verifies and measures again; a fix that costs the gain ends the climb.",
|
|
1235
|
-
...findings.map((finding) => `- ${finding.id}: ${finding.detail}`)
|
|
1236
|
-
].join(`
|
|
1237
|
-
|
|
1238
|
-
`);
|
|
1239
|
-
var percent = (fraction) => `${(fraction * 100).toFixed(1)}%`;
|
|
1240
|
-
var numbers = (measured) => [
|
|
1241
|
-
"| Metric | Role | Base median | Head median | Change | Noise |",
|
|
1242
|
-
"|---|---|---|---|---|---|",
|
|
1243
|
-
...measured.metrics.map((metric) => `| ${metric.name} | ${metric.role} | ${metric.base.median} | ${metric.head.median} | ${percent(metric.change)} | ${percent(metric.noise)} |`)
|
|
1244
|
-
].join(`
|
|
1245
|
-
`);
|
|
1246
|
-
var report = (input) => [
|
|
1247
|
-
`# Hillclimb findings
|
|
1248
|
-
|
|
1249
|
-
${input.reason}`,
|
|
1250
|
-
`## Stack
|
|
1251
|
-
|
|
1252
|
-
${input.discovery.stack}`,
|
|
1253
|
-
`## Telemetry
|
|
1254
|
-
|
|
1255
|
-
${input.discovery.telemetry.map((entry) => `- ${entry.source}: ${entry.status}, ${entry.why}`).join(`
|
|
1256
|
-
`) || "- None reached"}`,
|
|
1257
|
-
`## Benchmark
|
|
1258
|
-
|
|
1259
|
-
${input.harness.summary}`,
|
|
1260
|
-
`## Tried
|
|
1261
|
-
|
|
1262
|
-
${input.tried.map((entry) => `- ${entry}`).join(`
|
|
1263
|
-
`) || "- Nothing"}`,
|
|
1264
|
-
...input.kept.length === 0 ? [] : [`## Kept
|
|
1265
|
-
|
|
1266
|
-
${input.kept.map((entry) => `- ${entry.summary}: ${entry.reason}`).join(`
|
|
1267
|
-
`)}`],
|
|
1268
|
-
...input.final === undefined ? [] : [`## Final measurement
|
|
1269
|
-
|
|
1270
|
-
${input.final.reason}
|
|
1271
|
-
|
|
1272
|
-
${numbers(input.final)}`],
|
|
1273
|
-
`## Opportunities
|
|
1274
|
-
|
|
1275
|
-
${input.discovery.opportunities.map((entry) => `- ${entry.title} (${entry.evidence}${entry.external === true ? ", external" : ""}): ${entry.detail}`).join(`
|
|
1276
|
-
`)}`
|
|
1277
|
-
].join(`
|
|
1278
|
-
|
|
1279
|
-
`);
|
|
1280
|
-
var steadier = (plan) => {
|
|
1281
|
-
const counter = plan.metrics.find((metric) => metric.lowVariance === true && metric.role !== "primary");
|
|
1282
|
-
return counter === undefined ? undefined : {
|
|
1283
|
-
...plan,
|
|
1284
|
-
metrics: plan.metrics.map((metric) => ({
|
|
1285
|
-
...metric,
|
|
1286
|
-
role: metric.name === counter.name ? "primary" : "guardrail"
|
|
1287
|
-
}))
|
|
1288
|
-
};
|
|
1289
|
-
};
|
|
1290
|
-
var hillclimb_default = defineLoop({
|
|
1291
|
-
id: "hillclimb",
|
|
1292
|
-
version: "1.3.0",
|
|
1293
|
-
title: "Hillclimb",
|
|
1294
|
-
outcome: "performance",
|
|
1295
|
-
input: Input4,
|
|
1296
|
-
output: Output5,
|
|
1297
|
-
agents: { investigator: "climber", implementer: "implementer" },
|
|
1298
|
-
checkout: "write",
|
|
1299
|
-
capabilities: ["agent_work", "verify", "measure", "change_request", "write_back", "ask", "create_tasks"],
|
|
1300
|
-
calls: () => [review_default],
|
|
1301
|
-
offers: [{ id: "retry", title: "Climb again", loop: "hillclimb" }],
|
|
1302
|
-
run: (input) => gen(function* () {
|
|
1303
|
-
const limits = {
|
|
1304
|
-
...input.threshold === undefined ? undefined : { threshold: input.threshold },
|
|
1305
|
-
...input.tolerance === undefined ? undefined : { tolerance: input.tolerance }
|
|
1306
|
-
};
|
|
1307
|
-
const discovery = yield* agentWork("discover", {
|
|
1308
|
-
agent: "investigator",
|
|
1309
|
-
prompt: discoverPrompt(input.focus),
|
|
1310
|
-
output: Discovery
|
|
1311
|
-
});
|
|
1312
|
-
const external = discovery.opportunities.flatMap((entry) => entry.external === true ? [entry.title] : []);
|
|
1313
|
-
yield* syncBase("sync:start", {
|
|
1314
|
-
agent: "implementer",
|
|
1315
|
-
...input.branch === undefined ? undefined : { target: input.branch }
|
|
1316
|
-
});
|
|
1317
|
-
const harness = yield* agentWorkOnce("harness", {
|
|
1318
|
-
agent: "implementer",
|
|
1319
|
-
prompt: harnessPrompt(discovery),
|
|
1320
|
-
output: Harness
|
|
1321
|
-
});
|
|
1322
|
-
const baseline = (yield* verify("verify:harness", {})).revision;
|
|
1323
|
-
let plan = harness.plan;
|
|
1324
|
-
const calibrated = yield* measure("calibrate", { plan, base: baseline, head: baseline, ...limits });
|
|
1325
|
-
const kept = [];
|
|
1326
|
-
const tried = [];
|
|
1327
|
-
let cycles = 0;
|
|
1328
|
-
const ending = (status, reason, final) => gen(function* () {
|
|
1329
|
-
const stored = yield* artifact("report", {
|
|
1330
|
-
kind: "hillclimb-report",
|
|
1331
|
-
title: "Hillclimb findings",
|
|
1332
|
-
text: report({ discovery, harness, reason, kept, tried, final })
|
|
1333
|
-
});
|
|
1334
|
-
return { status, reason, kept, cycles, reportArtifactId: stored.artifactId, external };
|
|
1335
|
-
});
|
|
1336
|
-
if (calibrated.verdict === "too_noisy") {
|
|
1337
|
-
const switched = steadier(plan);
|
|
1338
|
-
if (switched === undefined)
|
|
1339
|
-
return yield* ending("too_noisy", `${calibrated.reason}. ${calibrated.suggestion ?? ""}`.trim());
|
|
1340
|
-
plan = switched;
|
|
1341
|
-
}
|
|
1342
|
-
let start = baseline;
|
|
1343
|
-
let unkept = 0;
|
|
1344
|
-
for (let cycle = 1;cycle <= (input.cycles ?? 5) && unkept < 2; cycle++) {
|
|
1345
|
-
const change = yield* agentWorkOnce(`change:${cycle}`, {
|
|
1346
|
-
agent: "implementer",
|
|
1347
|
-
prompt: changePrompt(discovery, plan, tried, cycle),
|
|
1348
|
-
output: Change
|
|
1349
|
-
});
|
|
1350
|
-
if (change.status !== "changed") {
|
|
1351
|
-
tried.push(`Cycle ${cycle}: ${change.status}, ${change.summary}`);
|
|
1352
|
-
break;
|
|
1353
|
-
}
|
|
1354
|
-
cycles = cycle;
|
|
1355
|
-
const head = yield* verify(`verify:${cycle}`, {});
|
|
1356
|
-
const measured = head.passed ? yield* measure(`measure:${cycle}`, { plan, base: start, head: head.revision, ...limits }) : undefined;
|
|
1357
|
-
if (measured?.verdict === "gain") {
|
|
1358
|
-
kept.push({ summary: change.summary, verdict: measured.verdict, reason: measured.reason });
|
|
1359
|
-
tried.push(`Cycle ${cycle}, kept: ${change.summary} (${measured.reason})`);
|
|
1360
|
-
start = head.revision;
|
|
1361
|
-
unkept = 0;
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
tried.push(`Cycle ${cycle}, reverted: ${change.summary} (${measured === undefined ? "its checks failed" : measured.reason})`);
|
|
1365
|
-
yield* agentWork(`revert:${cycle}`, {
|
|
1366
|
-
agent: "implementer",
|
|
1367
|
-
prompt: revertPrompt(start, cycle),
|
|
1368
|
-
output: Struct({ reverted: Boolean2 })
|
|
1369
|
-
});
|
|
1370
|
-
unkept++;
|
|
1371
|
-
}
|
|
1372
|
-
if (external.length > 0) {
|
|
1373
|
-
const file = yield* ask("external", {
|
|
1374
|
-
prompt: `Only other repositories can make these improvements: ${external.join("; ")}. File them as Tasks?`,
|
|
1375
|
-
options: ["File as Tasks", "Leave them"],
|
|
1376
|
-
recommended: "File as Tasks",
|
|
1377
|
-
recommendationReason: "Filed as Tasks, the improvements wait in the repositories that can make them instead of being lost.",
|
|
1378
|
-
default: "Leave them",
|
|
1379
|
-
timeout: "1 hour"
|
|
1380
|
-
});
|
|
1381
|
-
if (file === "File as Tasks")
|
|
1382
|
-
yield* createTasks("external-tasks", {
|
|
1383
|
-
tickets: discovery.opportunities.flatMap((entry, index) => entry.external === true ? [
|
|
1384
|
-
{
|
|
1385
|
-
key: `external-${index + 1}`,
|
|
1386
|
-
title: entry.title,
|
|
1387
|
-
body: entry.detail,
|
|
1388
|
-
acceptanceCriteria: [],
|
|
1389
|
-
blockedBy: [],
|
|
1390
|
-
outcome: "performance"
|
|
1391
|
-
}
|
|
1392
|
-
] : [])
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
if (kept.length === 0)
|
|
1396
|
-
return yield* ending("no_gain", "No change proved faster than the noise allows");
|
|
1397
|
-
const synced = yield* syncBase("sync:final", {
|
|
1398
|
-
agent: "implementer",
|
|
1399
|
-
...input.branch === undefined ? undefined : { target: input.branch }
|
|
1400
|
-
});
|
|
1401
|
-
const checked = yield* verify("verify:final", {});
|
|
1402
|
-
if (!checked.passed) {
|
|
1403
|
-
const failing = checked.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
1404
|
-
return yield* ending("checks_failed", `The final tree fails ${failing.join(", ") || "its checks"}`);
|
|
1405
|
-
}
|
|
1406
|
-
let final = yield* measure("measure:final", { plan, base: baseline, head: checked.revision, ...limits });
|
|
1407
|
-
if (final.verdict !== "gain")
|
|
1408
|
-
return yield* ending("gain_gone", `The final tree no longer proves the gain: ${final.reason}`, final);
|
|
1409
|
-
const claims = [
|
|
1410
|
-
"Behaviour is preserved: every kept change leaves what the code does exactly as it was.",
|
|
1411
|
-
`The measurement is valid: ${harness.summary}`,
|
|
1412
|
-
...kept.map((entry) => entry.summary)
|
|
1413
|
-
];
|
|
1414
|
-
let reviewed = yield* call("review", review_default, {
|
|
1415
|
-
base: baseline,
|
|
1416
|
-
head: checked.revision,
|
|
1417
|
-
outcome: "performance",
|
|
1418
|
-
claims
|
|
1419
|
-
});
|
|
1420
|
-
for (let round = 1;round <= 2 && reviewed.findings.some(isBlocking); round++) {
|
|
1421
|
-
const blocking = reviewed.findings.filter(isBlocking);
|
|
1422
|
-
yield* agentWork(`fix:${round}`, {
|
|
1423
|
-
agent: "implementer",
|
|
1424
|
-
prompt: fixPrompt2(blocking),
|
|
1425
|
-
output: Struct({ summary: String4 })
|
|
1426
|
-
});
|
|
1427
|
-
const fixed = yield* verify(`verify:fix:${round}`, {});
|
|
1428
|
-
if (!fixed.passed) {
|
|
1429
|
-
const failing = fixed.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
1430
|
-
return yield* ending("checks_failed", `The review's fixes fail ${failing.join(", ") || "the checks"}`);
|
|
1431
|
-
}
|
|
1432
|
-
final = yield* measure(`measure:fix:${round}`, { plan, base: baseline, head: fixed.revision, ...limits });
|
|
1433
|
-
if (final.verdict !== "gain")
|
|
1434
|
-
return yield* ending("gain_gone", `The review's fixes cost the gain: ${final.reason}`, final);
|
|
1435
|
-
reviewed = yield* call(`review:${round + 1}`, review_default, {
|
|
1436
|
-
base: baseline,
|
|
1437
|
-
head: fixed.revision,
|
|
1438
|
-
outcome: "performance",
|
|
1439
|
-
claims,
|
|
1440
|
-
earlier: blocking
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
const unsettled = [
|
|
1444
|
-
...[...reviewed.findings, ...scopeFindings(reviewed.scope)].flatMap((finding) => isBlocking(finding) ? [`${finding.id}: ${finding.detail}`] : []),
|
|
1445
|
-
...synced.status === "conflict" ? [`Merging the base in conflicts on ${synced.files.join(", ")}`] : []
|
|
1446
|
-
];
|
|
1447
|
-
const caveats = synced.status === "merged" ? ["The base moved during the climb; the final measurement includes its changes too."] : [];
|
|
1448
|
-
const opened = yield* changeRequest("change-request", {
|
|
1449
|
-
title: `Faster: ${kept.map((entry) => entry.summary).join("; ")}`.slice(0, 240),
|
|
1450
|
-
body: [
|
|
1451
|
-
`${final.reason}, measured by Kitman against the benchmark on the base.`,
|
|
1452
|
-
numbers(final),
|
|
1453
|
-
`## Kept
|
|
1454
|
-
|
|
1455
|
-
${kept.map((entry) => `- ${entry.summary}: ${entry.reason}`).join(`
|
|
1456
|
-
`)}`,
|
|
1457
|
-
`## Benchmark
|
|
1458
|
-
|
|
1459
|
-
${harness.summary}
|
|
1460
|
-
|
|
1461
|
-
\`${plan.command}\``,
|
|
1462
|
-
...unsettled.length + caveats.length === 0 ? [] : [`## Unsettled
|
|
1463
|
-
|
|
1464
|
-
${[...unsettled, ...caveats].map((entry) => `- ${entry}`).join(`
|
|
1465
|
-
`)}`]
|
|
1466
|
-
].join(`
|
|
1467
|
-
|
|
1468
|
-
`)
|
|
1469
|
-
});
|
|
1470
|
-
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
1471
|
-
return { ...yield* ending("improved", final.reason, final), changeRequest: opened, unsettled };
|
|
1472
|
-
})
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
// packages/loops/src/firewatch.ts
|
|
1476
|
-
var Input5 = Struct({
|
|
1477
|
-
alert: Record(String4, Json),
|
|
1478
|
-
repoBindingId: optional(String4),
|
|
1479
|
-
windowHours: optional(Int.check(isGreaterThanOrEqualTo(0), isLessThanOrEqualTo(720))),
|
|
1480
|
-
autoFix: optional(Boolean2)
|
|
1481
|
-
});
|
|
1482
|
-
var Conclusion = Struct({
|
|
1483
|
-
summary: String4,
|
|
1484
|
-
cause: String4,
|
|
1485
|
-
confidence: Literals(["high", "medium", "low"]),
|
|
1486
|
-
sources: NonEmptyArray(String4),
|
|
1487
|
-
telemetry: ArraySchema(Struct({ source: String4, reached: Boolean2, tried: optional(String4) })),
|
|
1488
|
-
inCode: Boolean2,
|
|
1489
|
-
fix: optional(String4)
|
|
1490
|
-
});
|
|
1491
|
-
var FixRequest = Struct({ words: String4, outcome: Literal("bug") });
|
|
1492
|
-
var Output6 = Struct({
|
|
1493
|
-
status: Literals(["folded", "resolved", "concluded"]),
|
|
1494
|
-
taskId: optional(String4),
|
|
1495
|
-
joinedRunId: optional(String4),
|
|
1496
|
-
conclusionArtifactId: optional(String4),
|
|
1497
|
-
resolvedDuring: Boolean2,
|
|
1498
|
-
fix: Literals(["none", "declined", "started", "failed"]),
|
|
1499
|
-
changeRequestUrl: optional(String4),
|
|
1500
|
-
fixRequest: optional(FixRequest)
|
|
1501
|
-
});
|
|
1502
|
-
var firstText = (alert, ...keys) => keys.flatMap((key) => {
|
|
1503
|
-
const value = alert[key];
|
|
1504
|
-
return isString2(value) && value.trim() !== "" ? [value] : [];
|
|
1505
|
-
})[0];
|
|
1506
|
-
var alertTitle = (alert) => firstText(alert, "title", "name", "summary") ?? "Alert";
|
|
1507
|
-
var fingerprintOf = (alert) => firstText(alert, "fingerprint", "groupKey") ?? alertTitle(alert);
|
|
1508
|
-
var isResolved = (alert) => firstText(alert, "status", "state")?.toLowerCase() === "resolved";
|
|
1509
|
-
var priorities = new Map(Object.entries({
|
|
1510
|
-
critical: "urgent",
|
|
1511
|
-
p1: "urgent",
|
|
1512
|
-
urgent: "urgent",
|
|
1513
|
-
high: "high",
|
|
1514
|
-
error: "high",
|
|
1515
|
-
p2: "high",
|
|
1516
|
-
warning: "medium",
|
|
1517
|
-
medium: "medium",
|
|
1518
|
-
p3: "medium",
|
|
1519
|
-
low: "low",
|
|
1520
|
-
info: "low",
|
|
1521
|
-
p4: "low"
|
|
1522
|
-
}));
|
|
1523
|
-
var priorityOf = (alert) => {
|
|
1524
|
-
const severity = firstText(alert, "severity", "priority", "level")?.toLowerCase();
|
|
1525
|
-
return (severity === undefined ? undefined : priorities.get(severity)) ?? "high";
|
|
1526
|
-
};
|
|
1527
|
-
var investigatePrompt = (alert) => [
|
|
1528
|
-
"Investigate this alert and conclude what happened and why.",
|
|
1529
|
-
"```json",
|
|
1530
|
-
JSON.stringify(alert, null, 2),
|
|
1531
|
-
"```",
|
|
1532
|
-
"Cite as sources the logs, metrics, commits or code that support your conclusion. List every telemetry source you tried and whether you could read it, with what you tried for one you could not; say how confident you are, lower when a source was out of reach.",
|
|
1533
|
-
"Say whether the cause is in code Kitman can change, and propose a fix only then."
|
|
1534
|
-
].join(`
|
|
1535
|
-
|
|
1536
|
-
`);
|
|
1537
|
-
var conclusionText = (conclusion, resolvedDuring) => [
|
|
1538
|
-
conclusion.summary,
|
|
1539
|
-
...resolvedDuring ? ["It resolved on its own while it was being investigated."] : [],
|
|
1540
|
-
`Cause: ${conclusion.cause} (confidence ${conclusion.confidence})`,
|
|
1541
|
-
"Sources:",
|
|
1542
|
-
...conclusion.sources.map((source) => `- ${source}`),
|
|
1543
|
-
...conclusion.telemetry.some((source) => !source.reached) ? [
|
|
1544
|
-
"Out of reach:",
|
|
1545
|
-
...conclusion.telemetry.filter((source) => !source.reached).map((source) => `- ${source.source}${source.tried === undefined ? "" : `: ${source.tried}`}`)
|
|
1546
|
-
] : [],
|
|
1547
|
-
...conclusion.fix === undefined ? [] : [`Fix: ${conclusion.fix}`]
|
|
1548
|
-
].join(`
|
|
1549
|
-
`);
|
|
1550
|
-
var firewatch_default = defineLoop({
|
|
1551
|
-
id: "firewatch",
|
|
1552
|
-
version: "1.0.0",
|
|
1553
|
-
title: "Firewatch",
|
|
1554
|
-
input: Input5,
|
|
1555
|
-
output: Output6,
|
|
1556
|
-
outcome: "investigation",
|
|
1557
|
-
agents: { investigator: "investigator" },
|
|
1558
|
-
checkout: "read",
|
|
1559
|
-
when: [{ event: "monitoring.alert", input: { alert: "alert" } }],
|
|
1560
|
-
offers: [{ id: "build-fix", title: "Build the fix", loop: "build", from: "fixRequest" }],
|
|
1561
|
-
capabilities: ["agent_work", "ask", "create_tasks"],
|
|
1562
|
-
calls: () => [build_default],
|
|
1563
|
-
run: (input) => gen(function* () {
|
|
1564
|
-
const fingerprint = fingerprintOf(input.alert);
|
|
1565
|
-
const resolved = isResolved(input.alert);
|
|
1566
|
-
const windowHours = input.windowHours ?? 6;
|
|
1567
|
-
const request = { fingerprint, title: alertTitle(input.alert), windowHours, priority: priorityOf(input.alert) };
|
|
1568
|
-
const placed = yield* alertTask("task", { ...request, ...resolved ? { resolved } : undefined });
|
|
1569
|
-
const taskId = placed.taskId === undefined ? undefined : { taskId: placed.taskId };
|
|
1570
|
-
if (placed.joined !== undefined)
|
|
1571
|
-
return {
|
|
1572
|
-
status: "folded",
|
|
1573
|
-
...taskId,
|
|
1574
|
-
joinedRunId: placed.joined.runId,
|
|
1575
|
-
resolvedDuring: false,
|
|
1576
|
-
fix: "none"
|
|
1577
|
-
};
|
|
1578
|
-
if (resolved)
|
|
1579
|
-
return { status: "resolved", ...taskId, resolvedDuring: false, fix: "none" };
|
|
1580
|
-
const conclusion = yield* agentWork("investigate", {
|
|
1581
|
-
agent: "investigator",
|
|
1582
|
-
prompt: investigatePrompt(input.alert),
|
|
1583
|
-
output: Conclusion
|
|
1584
|
-
});
|
|
1585
|
-
const resolvedDuring = (yield* alertTask("task:check", request)).resolved === true;
|
|
1586
|
-
const stored = yield* artifact("conclusion", {
|
|
1587
|
-
kind: "investigation_conclusion",
|
|
1588
|
-
title: request.title,
|
|
1589
|
-
text: conclusionText(conclusion, resolvedDuring),
|
|
1590
|
-
data: { alert: input.alert, sources: conclusion.sources, confidence: conclusion.confidence }
|
|
1591
|
-
});
|
|
1592
|
-
yield* note("summary", { text: conclusion.summary, artifactId: stored.artifactId });
|
|
1593
|
-
const concluded = {
|
|
1594
|
-
status: "concluded",
|
|
1595
|
-
...taskId,
|
|
1596
|
-
conclusionArtifactId: stored.artifactId,
|
|
1597
|
-
resolvedDuring
|
|
1598
|
-
};
|
|
1599
|
-
if (conclusion.fix === undefined || !conclusion.inCode)
|
|
1600
|
-
return { ...concluded, fix: "none" };
|
|
1601
|
-
const fixRequest = {
|
|
1602
|
-
words: `${conclusion.fix}
|
|
1603
|
-
|
|
1604
|
-
${conclusionText(conclusion, resolvedDuring)}`,
|
|
1605
|
-
outcome: "bug"
|
|
1606
|
-
};
|
|
1607
|
-
const confident = conclusion.confidence === "high" && !resolvedDuring;
|
|
1608
|
-
const answer = yield* ask("start-fix", {
|
|
1609
|
-
prompt: `${conclusion.summary}
|
|
1610
|
-
|
|
1611
|
-
Start a Build that fixes it as a bug: ${conclusion.fix}?`,
|
|
1612
|
-
options: [
|
|
1613
|
-
{ value: "Start a fix", description: "A Build fixes it as a bug and opens a ChangeRequest." },
|
|
1614
|
-
{ value: "Not now", description: "Keeps the finding on the Task." }
|
|
1615
|
-
],
|
|
1616
|
-
recommended: confident ? "Start a fix" : "Not now",
|
|
1617
|
-
recommendationReason: confident ? "Firewatch is confident of the cause and the alert is still firing." : resolvedDuring ? "The alert resolved while Firewatch looked, so a fix can wait for a person to judge." : "Firewatch is not confident of the cause, so a fix could aim at the wrong thing.",
|
|
1618
|
-
default: input.autoFix === true && confident ? "Start a fix" : "Not now",
|
|
1619
|
-
timeout: "1 hour"
|
|
1620
|
-
});
|
|
1621
|
-
if (answer === "Not now")
|
|
1622
|
-
return { ...concluded, fix: "declined", fixRequest };
|
|
1623
|
-
return yield* call("fix", build_default, {
|
|
1624
|
-
...fixRequest,
|
|
1625
|
-
...taskId,
|
|
1626
|
-
...input.repoBindingId === undefined ? undefined : { repoBindingId: input.repoBindingId }
|
|
1627
|
-
}).pipe(map8((fixed) => ({ ...concluded, fix: "started", changeRequestUrl: fixed.changeRequest.url })), catchTag("ChildFailed", () => succeed6({ ...concluded, fix: "failed", fixRequest })));
|
|
1628
|
-
})
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
|
-
// packages/loops/src/plan.ts
|
|
1632
|
-
var Input6 = Struct({
|
|
1633
|
-
words: String4,
|
|
1634
|
-
repoBindingId: optional(String4),
|
|
1635
|
-
branch: optional(String4),
|
|
1636
|
-
projectId: optional(String4),
|
|
1637
|
-
implement: optional(Boolean2)
|
|
1638
|
-
});
|
|
1639
|
-
var Plan = Struct({
|
|
1640
|
-
title: String4,
|
|
1641
|
-
spec: String4,
|
|
1642
|
-
tickets: PlanTickets,
|
|
1643
|
-
outOfScope: ArraySchema(String4),
|
|
1644
|
-
afterMerge: ArraySchema(String4),
|
|
1645
|
-
docs: optional(Struct({ title: String4, summary: String4 }))
|
|
1646
|
-
});
|
|
1647
|
-
var CriteriaCheck = Struct({
|
|
1648
|
-
alreadyMet: ArraySchema(Struct({ key: String4, criterion: String4 }))
|
|
1649
|
-
});
|
|
1650
|
-
var Rejected = Struct({ key: String4, reason: String4 });
|
|
1651
|
-
var Opinion = Struct({ summary: String4, concerns: ArraySchema(String4) });
|
|
1652
|
-
var TicketResult = Struct({
|
|
1653
|
-
key: String4,
|
|
1654
|
-
title: String4,
|
|
1655
|
-
status: Literals(["merged", "failed", "skipped"]),
|
|
1656
|
-
repoBindingId: optional(String4),
|
|
1657
|
-
changeRequest: optional(ChangeRequestResult),
|
|
1658
|
-
reason: optional(String4)
|
|
1659
|
-
});
|
|
1660
|
-
var Output7 = Struct({
|
|
1661
|
-
title: String4,
|
|
1662
|
-
outOfScope: ArraySchema(String4),
|
|
1663
|
-
afterMerge: ArraySchema(String4),
|
|
1664
|
-
rejected: ArraySchema(Rejected),
|
|
1665
|
-
stopped: optional(String4),
|
|
1666
|
-
implemented: Boolean2,
|
|
1667
|
-
tickets: ArraySchema(TicketResult),
|
|
1668
|
-
deliveries: ArraySchema(ChangeRequestResult),
|
|
1669
|
-
tasks: optional(CreateTasksResult.fields.tasks),
|
|
1670
|
-
docs: optional(ChangeRequestResult)
|
|
1671
|
-
});
|
|
1672
|
-
var menu = ["Implement now", "File as Tasks", "Second opinion", "Refine", "Finish"];
|
|
1673
|
-
var maxRounds = 6;
|
|
1674
|
-
var maxOpinionsPerPlan = 2;
|
|
1675
|
-
var menuDescriptions = {
|
|
1676
|
-
"Implement now": "Builds the tickets in dependency order and delivers them.",
|
|
1677
|
-
"File as Tasks": "Makes a Task of each ticket, to build later.",
|
|
1678
|
-
"Second opinion": "Another Agent reviews the plan.",
|
|
1679
|
-
Refine: "You say what to change and the planner rewrites it.",
|
|
1680
|
-
Finish: "Ends the Run with the plan as it is."
|
|
1681
|
-
};
|
|
1682
|
-
var onFailure = ["Retry with a steer", "Skip it and its dependents", "Stop here"];
|
|
1683
|
-
var ticketRules = [
|
|
1684
|
-
"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; tickets never block each other in a cycle. Set repoBindingId only when it changes another repository than the plan, keep one repository per ticket, and no cycle across repositories. Set outcome to the kind of change: feature, bug, refactor, docs, migration, investigation or performance.",
|
|
1685
|
-
"Each acceptance criterion is something a Build can show before its ChangeRequest: a behaviour a test or a check proves. A text search is never a criterion, nor is something the code already does. Outcomes only visible after merge go under afterMerge, not under a ticket's criteria."
|
|
1686
|
-
].join(`
|
|
1687
|
-
|
|
1688
|
-
`);
|
|
1689
|
-
var planPrompt = (words) => [
|
|
1690
|
-
`Plan this: ${words}`,
|
|
1691
|
-
"You run the grill-with-docs skill as a Kitman Step. Interview the person with the Kitman MCP ask tool, one question at a time, and ask only what changes what gets built: a clear goal needs no interview. Write the ADR and glossary changes the grilling settles in the checkout, and no other files.",
|
|
1692
|
-
"Then write the spec and the tickets from the goal, the code and the answers.",
|
|
1693
|
-
ticketRules,
|
|
1694
|
-
"List in outOfScope what the plan deliberately leaves out, one item each; each holds the Builds of its tickets.",
|
|
1695
|
-
"If you left ADR or glossary changes, give docs a ChangeRequest title and a summary of the decisions; Kitman opens it. Your Output is the plan, and Kitman stores it."
|
|
1696
|
-
].join(`
|
|
1697
|
-
|
|
1698
|
-
`);
|
|
1699
|
-
var planText = (plan) => [
|
|
1700
|
-
`# ${plan.title}`,
|
|
1701
|
-
plan.spec,
|
|
1702
|
-
...plan.outOfScope.length === 0 ? [] : ["## Out of scope", plan.outOfScope.map((item) => `- ${item}`).join(`
|
|
1703
|
-
`)],
|
|
1704
|
-
...plan.afterMerge.length === 0 ? [] : ["## After merge", plan.afterMerge.map((item) => `- ${item}`).join(`
|
|
1705
|
-
`)],
|
|
1706
|
-
"## Tickets",
|
|
1707
|
-
...plan.tickets.map(ticketText)
|
|
1708
|
-
].join(`
|
|
1709
|
-
|
|
1710
|
-
`);
|
|
1711
|
-
var ticketText = (ticket) => [
|
|
1712
|
-
`### ${ticket.key}: ${ticket.title}`,
|
|
1713
|
-
ticket.body,
|
|
1714
|
-
...ticket.acceptanceCriteria.map((criterion) => `- [ ] ${criterion}`),
|
|
1715
|
-
...ticket.blockedBy.length === 0 ? [] : [`Blocked by: ${ticket.blockedBy.join(", ")}`]
|
|
1716
|
-
].join(`
|
|
1717
|
-
|
|
1718
|
-
`);
|
|
1719
|
-
var opinionPrompt = (plan) => [
|
|
1720
|
-
"Review this plan as a second opinion. Look for missing scope, tickets too big to review, wrong blockers and risks. Change nothing.",
|
|
1721
|
-
planText(plan)
|
|
1722
|
-
].join(`
|
|
1723
|
-
|
|
1724
|
-
`);
|
|
1725
|
-
var refinePrompt = (plan, words) => [`Change the plan as the user asks: ${words}`, ticketRules, planText(plan)].join(`
|
|
1726
|
-
|
|
1727
|
-
`);
|
|
1728
|
-
var criteriaPrompt2 = (plan) => [
|
|
1729
|
-
"Check each ticket's acceptance criteria against the base branch as it is now. List under alreadyMet each criterion the code already meets, with its ticket's key and the criterion word for word. Change nothing.",
|
|
1730
|
-
planText(plan)
|
|
1731
|
-
].join(`
|
|
1732
|
-
|
|
1733
|
-
`);
|
|
1734
|
-
var guard = " (guard: the base already does this)";
|
|
1735
|
-
var guarded = (plan, check) => {
|
|
1736
|
-
const met = (ticket, criterion) => check.alreadyMet.some((entry) => entry.key === ticket.key && entry.criterion === criterion);
|
|
1737
|
-
const rejected = plan.tickets.flatMap((ticket) => ticket.acceptanceCriteria.length > 0 && ticket.acceptanceCriteria.every((criterion) => met(ticket, criterion)) ? [{ key: ticket.key, reason: "The base branch already meets every one of its criteria" }] : []);
|
|
1738
|
-
const gone = new Set(rejected.map((entry) => entry.key));
|
|
1739
|
-
const tickets = plan.tickets.flatMap((ticket) => gone.has(ticket.key) ? [] : [
|
|
1740
|
-
{
|
|
1741
|
-
...ticket,
|
|
1742
|
-
acceptanceCriteria: ticket.acceptanceCriteria.map((criterion) => met(ticket, criterion) ? `${criterion}${guard}` : criterion),
|
|
1743
|
-
blockedBy: ticket.blockedBy.filter((key) => !gone.has(key))
|
|
1744
|
-
}
|
|
1745
|
-
]);
|
|
1746
|
-
return { plan: { ...plan, tickets }, rejected };
|
|
1747
|
-
};
|
|
1748
|
-
var checked = (plan, revision) => map8(agentWork(`criteria-check:${revision}`, { agent: "reviewer", prompt: criteriaPrompt2(plan), output: CriteriaCheck }), (check) => guarded(plan, check));
|
|
1749
|
-
var storePlan = (plan, suffix) => gen(function* () {
|
|
1750
|
-
yield* artifact(`spec${suffix}`, {
|
|
1751
|
-
kind: "plan_spec",
|
|
1752
|
-
title: plan.title,
|
|
1753
|
-
text: plan.spec,
|
|
1754
|
-
data: { outOfScope: [...plan.outOfScope] }
|
|
1755
|
-
});
|
|
1756
|
-
yield* artifact(`tickets${suffix}`, {
|
|
1757
|
-
kind: "plan_tickets",
|
|
1758
|
-
title: `Tickets for ${plan.title}`,
|
|
1759
|
-
text: plan.tickets.map(ticketText).join(`
|
|
1760
|
-
|
|
1761
|
-
`),
|
|
1762
|
-
data: plan.tickets.map(({ repoBindingId, outcome, ...ticket }) => ({
|
|
1763
|
-
...ticket,
|
|
1764
|
-
acceptanceCriteria: [...ticket.acceptanceCriteria],
|
|
1765
|
-
blockedBy: [...ticket.blockedBy],
|
|
1766
|
-
...repoBindingId === undefined ? undefined : { repoBindingId },
|
|
1767
|
-
...outcome === undefined ? undefined : { outcome }
|
|
1768
|
-
}))
|
|
1769
|
-
});
|
|
1770
|
-
});
|
|
1771
|
-
var changelog = (before, after, words) => {
|
|
1772
|
-
const was = new Map(before.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
1773
|
-
const now = new Map(after.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
1774
|
-
const lines = [
|
|
1775
|
-
...after.tickets.filter((ticket) => !was.has(ticket.key)).map((ticket) => `- Added ${ticket.key}: ${ticket.title}`),
|
|
1776
|
-
...before.tickets.filter((ticket) => !now.has(ticket.key)).map((ticket) => `- Removed ${ticket.key}: ${ticket.title}`),
|
|
1777
|
-
...after.tickets.filter((ticket) => was.has(ticket.key) && was.get(ticket.key) !== now.get(ticket.key)).map((ticket) => `- Changed ${ticket.key}: ${ticket.title}`),
|
|
1778
|
-
...before.spec === after.spec ? [] : ["- Changed the spec"],
|
|
1779
|
-
...before.title === after.title ? [] : [`- Renamed the plan from "${before.title}"`]
|
|
1780
|
-
];
|
|
1781
|
-
return [`Asked: ${words}`, ...lines.length === 0 ? ["Nothing changed."] : lines].join(`
|
|
1782
|
-
`);
|
|
1783
|
-
};
|
|
1784
|
-
var buildWords = (ticket) => [
|
|
1785
|
-
ticket.title,
|
|
1786
|
-
ticket.body,
|
|
1787
|
-
...ticket.acceptanceCriteria.length === 0 ? [] : ["Acceptance criteria:", ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)]
|
|
1788
|
-
].join(`
|
|
1789
|
-
|
|
1790
|
-
`);
|
|
1791
|
-
var settled = (ticket, result) => ({
|
|
1792
|
-
key: ticket.key,
|
|
1793
|
-
title: ticket.title,
|
|
1794
|
-
...result
|
|
1795
|
-
});
|
|
1796
|
-
var buildTicket = (ticket, start, taskId, retry) => gen(function* () {
|
|
1797
|
-
let head = start;
|
|
1798
|
-
const name = retry === undefined ? ticket.key : `${ticket.key}:${retry.label}`;
|
|
1799
|
-
for (let attempt = 1;; attempt++) {
|
|
1800
|
-
const suffix = attempt === 1 ? "" : `:${attempt}`;
|
|
1801
|
-
const words = retry === undefined ? taskId === undefined ? { words: buildWords(ticket) } : undefined : { words: `${buildWords(ticket)}
|
|
1802
|
-
|
|
1803
|
-
An earlier Build of this failed. ${retry.steer}` };
|
|
1804
|
-
const built = yield* call(`build:${name}${suffix}`, build_default, {
|
|
1805
|
-
...words,
|
|
1806
|
-
...taskId === undefined ? undefined : { taskId },
|
|
1807
|
-
repoBindingId: head.repoBindingId,
|
|
1808
|
-
branch: head.branch,
|
|
1809
|
-
revision: head.revision
|
|
1810
|
-
}).pipe(catchTag("ChildFailed", (failure) => succeed6(failure)));
|
|
1811
|
-
if (built instanceof ChildFailed)
|
|
1812
|
-
return settled(ticket, { status: "failed", repoBindingId: head.repoBindingId, reason: built.reason });
|
|
1813
|
-
const merged = yield* mergeChangeRequest(`merge:${name}${suffix}`, {
|
|
1814
|
-
changeRequestId: built.changeRequest.id,
|
|
1815
|
-
head: built.revision
|
|
1816
|
-
});
|
|
1817
|
-
const delivered = { repoBindingId: head.repoBindingId, changeRequest: built.changeRequest };
|
|
1818
|
-
if (merged.status === "merged")
|
|
1819
|
-
return settled(ticket, { status: "merged", ...delivered });
|
|
1820
|
-
const choice = yield* ask(`conflict:${name}${suffix}`, {
|
|
1821
|
-
prompt: `The ChangeRequest for ${ticket.key} (${built.changeRequest.url}) ${merged.status === "moved" ? "changed after it was built" : `conflicts with ${head.branch}`}. Rebuild ${ticket.key} on the integration branch as it is now, or skip it and the tickets it blocks?`,
|
|
1822
|
-
options: [
|
|
1823
|
-
{
|
|
1824
|
-
value: "Rebuild on the integration branch",
|
|
1825
|
-
description: "Builds the ticket again from the branch as it is now."
|
|
1826
|
-
},
|
|
1827
|
-
{ value: "Skip", description: "Leaves it and the tickets it blocks undelivered." }
|
|
1828
|
-
],
|
|
1829
|
-
recommended: "Rebuild on the integration branch",
|
|
1830
|
-
recommendationReason: "A rebuild on the current branch usually resolves a conflict, and skipping also drops every ticket it blocks.",
|
|
1831
|
-
default: "Skip",
|
|
1832
|
-
timeout: "1 hour"
|
|
1833
|
-
});
|
|
1834
|
-
if (choice === "Skip")
|
|
1835
|
-
return settled(ticket, { status: "failed", ...delivered, reason: `It conflicted with ${head.branch}` });
|
|
1836
|
-
head = yield* integrationBranch(`rebuild-from:${name}:${attempt + 1}`, {
|
|
1837
|
-
repoBindingId: head.repoBindingId
|
|
1838
|
-
});
|
|
1839
|
-
}
|
|
1840
|
-
});
|
|
1841
|
-
var deliveryBody = (plan, results) => {
|
|
1842
|
-
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}`;
|
|
1843
|
-
const section = (title, status) => {
|
|
1844
|
-
const lines = results.flatMap((result) => result.status === status ? [line(result)] : []);
|
|
1845
|
-
return lines.length === 0 ? [] : [`## ${title}`, lines.join(`
|
|
1846
|
-
`)];
|
|
1847
|
-
};
|
|
1848
|
-
return [
|
|
1849
|
-
`Delivers the plan "${plan.title}".`,
|
|
1850
|
-
...section("Merged", "merged"),
|
|
1851
|
-
...section("Failed", "failed"),
|
|
1852
|
-
...section("Skipped", "skipped")
|
|
1853
|
-
].join(`
|
|
1854
|
-
|
|
1855
|
-
`);
|
|
1856
|
-
};
|
|
1857
|
-
var implement = (input, plan, revision, filed) => gen(function* () {
|
|
1858
|
-
const tasks = filed?.revision === revision ? filed.tasks : (yield* createTasks("tasks", {
|
|
1859
|
-
tickets: plan.tickets,
|
|
1860
|
-
...input.projectId === undefined ? undefined : { projectId: input.projectId }
|
|
1861
|
-
})).tasks;
|
|
1862
|
-
const taskOf = new Map(tasks.map((task) => [task.key, task.taskId]));
|
|
1863
|
-
const results = new Map;
|
|
1864
|
-
const waves = yield* planWaves(plan.tickets);
|
|
1865
|
-
let stopped;
|
|
1866
|
-
for (const [index, wave] of waves.entries()) {
|
|
1867
|
-
const ready = [];
|
|
1868
|
-
for (const ticket of wave) {
|
|
1869
|
-
const unmerged = ticket.blockedBy.find((key) => results.get(key)?.status !== "merged");
|
|
1870
|
-
if (stopped !== undefined)
|
|
1871
|
-
results.set(ticket.key, settled(ticket, { status: "skipped", reason: stopped }));
|
|
1872
|
-
else if (unmerged === undefined)
|
|
1873
|
-
ready.push(ticket);
|
|
1874
|
-
else
|
|
1875
|
-
results.set(ticket.key, settled(ticket, { status: "skipped", reason: `blocked by ${unmerged}, which was not merged` }));
|
|
1876
|
-
}
|
|
1877
|
-
const repoOf = (ticket) => ticket.repoBindingId ?? input.repoBindingId;
|
|
1878
|
-
const heads = new Map;
|
|
1879
|
-
for (const repo of new Set(ready.map(repoOf)))
|
|
1880
|
-
heads.set(repo, yield* integrationBranch(`integration:wave-${index + 1}:${repo ?? "default"}`, repo === undefined ? {} : { repoBindingId: repo }));
|
|
1881
|
-
const built = yield* forEach(ready.flatMap((ticket) => {
|
|
1882
|
-
const head = heads.get(repoOf(ticket));
|
|
1883
|
-
return head === undefined ? [] : [buildTicket(ticket, head, taskOf.get(ticket.key))];
|
|
1884
|
-
}), (building) => building, { concurrency: "unbounded" });
|
|
1885
|
-
for (const first of built) {
|
|
1886
|
-
const ticket = ready.find((entry) => entry.key === first.key);
|
|
1887
|
-
let result = first;
|
|
1888
|
-
for (let retry = 1;ticket !== undefined && result.status === "failed" && stopped === undefined; retry++) {
|
|
1889
|
-
const choice = yield* ask(`failed:${ticket.key}:${retry}`, {
|
|
1890
|
-
prompt: `The Build of ${ticket.key} failed: ${result.reason ?? "no reason given"}. Retry it with a steer, skip it and the tickets it blocks, or stop here?`,
|
|
1891
|
-
options: onFailure,
|
|
1892
|
-
recommended: "Retry with a steer",
|
|
1893
|
-
recommendationReason: "The reason it failed is above, so a steer can aim the retry at it; skipping also drops the tickets it blocks.",
|
|
1894
|
-
default: "Skip it and its dependents",
|
|
1895
|
-
timeout: "1 hour"
|
|
1896
|
-
});
|
|
1897
|
-
if (choice === "Skip it and its dependents")
|
|
1898
|
-
break;
|
|
1899
|
-
if (choice === "Stop here") {
|
|
1900
|
-
stopped = `Stopped after ${ticket.key} failed`;
|
|
1901
|
-
break;
|
|
1902
|
-
}
|
|
1903
|
-
const steer = yield* ask(`steer:${ticket.key}:${retry}`, {
|
|
1904
|
-
prompt: `What should the retry of ${ticket.key} do differently?`,
|
|
1905
|
-
freeText: true,
|
|
1906
|
-
recommended: `Address the failure: ${result.reason ?? "find out why it failed first"}`,
|
|
1907
|
-
recommendationReason: "Pointing the retry at the reason it failed is the least a steer should do.",
|
|
1908
|
-
default: null
|
|
1909
|
-
});
|
|
1910
|
-
const head = yield* integrationBranch(`integration:${ticket.key}:retry-${retry}`, {
|
|
1911
|
-
...repoOf(ticket) === undefined ? undefined : { repoBindingId: repoOf(ticket) }
|
|
1912
|
-
});
|
|
1913
|
-
result = yield* buildTicket(ticket, head, taskOf.get(ticket.key), { label: `retry-${retry}`, steer });
|
|
1914
|
-
}
|
|
1915
|
-
results.set(result.key, result);
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
const ordered = plan.tickets.flatMap((ticket) => results.get(ticket.key) ?? []);
|
|
1919
|
-
const repos = [
|
|
1920
|
-
...new Set(ordered.flatMap((result) => result.status === "merged" ? result.repoBindingId ?? [] : []))
|
|
1921
|
-
];
|
|
1922
|
-
const deliveries = yield* forEach(repos, (repo) => changeRequest(`deliver:${repo}`, {
|
|
1923
|
-
title: plan.title,
|
|
1924
|
-
body: deliveryBody(plan, ordered.filter((result) => result.repoBindingId === repo || result.repoBindingId === undefined)),
|
|
1925
|
-
integration: repo
|
|
1926
|
-
}));
|
|
1927
|
-
return { tickets: ordered, deliveries, tasks, ...stopped === undefined ? undefined : { stopped } };
|
|
1928
|
-
});
|
|
1929
|
-
var plan_default = defineLoop({
|
|
1930
|
-
id: "plan",
|
|
1931
|
-
version: "2.0.0",
|
|
1932
|
-
title: "Plan",
|
|
1933
|
-
input: Input6,
|
|
1934
|
-
output: Output7,
|
|
1935
|
-
agents: { planner: "planner", reviewer: "reviewer" },
|
|
1936
|
-
checkout: "write",
|
|
1937
|
-
capabilities: [
|
|
1938
|
-
"agent_work",
|
|
1939
|
-
"ask",
|
|
1940
|
-
"call",
|
|
1941
|
-
"verify",
|
|
1942
|
-
"change_request",
|
|
1943
|
-
"write_back",
|
|
1944
|
-
"create_tasks",
|
|
1945
|
-
"merge_change_request",
|
|
1946
|
-
"read_change_request",
|
|
1947
|
-
"post_comment"
|
|
1948
|
-
],
|
|
1949
|
-
calls: () => [build_default],
|
|
1950
|
-
run: (input) => gen(function* () {
|
|
1951
|
-
const drafted = yield* agentWorkOnce("plan", {
|
|
1952
|
-
agent: "planner",
|
|
1953
|
-
skill: "grill-with-docs",
|
|
1954
|
-
prompt: planPrompt(input.words),
|
|
1955
|
-
output: Plan
|
|
1956
|
-
});
|
|
1957
|
-
const docs = drafted.docs === undefined ? undefined : yield* changeRequest("docs", { title: drafted.docs.title, body: drafted.docs.summary });
|
|
1958
|
-
let revision = 1;
|
|
1959
|
-
let { plan, rejected } = yield* checked(drafted, revision);
|
|
1960
|
-
let filed;
|
|
1961
|
-
let opinions = 0;
|
|
1962
|
-
yield* storePlan(plan, "");
|
|
1963
|
-
const project = input.projectId === undefined ? undefined : { projectId: input.projectId };
|
|
1964
|
-
const ended = (rest) => ({
|
|
1965
|
-
title: plan.title,
|
|
1966
|
-
outOfScope: plan.outOfScope,
|
|
1967
|
-
afterMerge: plan.afterMerge,
|
|
1968
|
-
rejected,
|
|
1969
|
-
tickets: [],
|
|
1970
|
-
deliveries: [],
|
|
1971
|
-
...filed === undefined ? undefined : { tasks: filed.tasks },
|
|
1972
|
-
...docs === undefined ? undefined : { docs },
|
|
1973
|
-
...rest
|
|
1974
|
-
});
|
|
1975
|
-
for (let round = 1;round <= maxRounds; round++) {
|
|
1976
|
-
const isFiled = filed?.revision === revision;
|
|
1977
|
-
const options = menu.filter((option) => !(option === "File as Tasks" && isFiled) && !(option === "Second opinion" && opinions >= maxOpinionsPerPlan));
|
|
1978
|
-
const fallback = input.implement === true ? "Implement now" : isFiled ? "Finish" : "File as Tasks";
|
|
1979
|
-
const answer = yield* ask(`next:${round}`, {
|
|
1980
|
-
prompt: `What next for the plan "${plan.title}"?`,
|
|
1981
|
-
options: options.map((option) => ({ value: option, description: menuDescriptions[option] })),
|
|
1982
|
-
recommended: fallback,
|
|
1983
|
-
recommendationReason: fallback === "Implement now" ? "You asked for the plan to be implemented." : fallback === "Finish" ? "The plan is filed as Tasks, so nothing is lost by finishing." : "Filing keeps the plan as Tasks a person or a Build can pick up later.",
|
|
1984
|
-
default: fallback,
|
|
1985
|
-
timeout: "1 hour"
|
|
1986
|
-
});
|
|
1987
|
-
const choice = menu.find((option) => option === answer) ?? fallback;
|
|
1988
|
-
switch (choice) {
|
|
1989
|
-
case "Finish":
|
|
1990
|
-
return ended({ implemented: false });
|
|
1991
|
-
case "Implement now": {
|
|
1992
|
-
const delivered = yield* implement(input, plan, revision, filed);
|
|
1993
|
-
return { ...ended({ implemented: true }), ...delivered };
|
|
1994
|
-
}
|
|
1995
|
-
case "File as Tasks": {
|
|
1996
|
-
const made = yield* createTasks(`file-tasks:${round}`, { tickets: plan.tickets, ...project });
|
|
1997
|
-
filed = { revision, tasks: made.tasks };
|
|
1998
|
-
break;
|
|
1999
|
-
}
|
|
2000
|
-
case "Second opinion": {
|
|
2001
|
-
opinions++;
|
|
2002
|
-
const opinion = yield* agentWork(`second-opinion:${round}`, {
|
|
2003
|
-
agent: "reviewer",
|
|
2004
|
-
prompt: opinionPrompt(plan),
|
|
2005
|
-
output: Opinion
|
|
2006
|
-
});
|
|
2007
|
-
yield* artifact(`opinion:${round}`, {
|
|
2008
|
-
kind: "plan_review",
|
|
2009
|
-
title: `Second opinion on ${plan.title}`,
|
|
2010
|
-
text: [opinion.summary, ...opinion.concerns.map((concern) => `- ${concern}`)].join(`
|
|
2011
|
-
|
|
2012
|
-
`)
|
|
2013
|
-
});
|
|
2014
|
-
break;
|
|
2015
|
-
}
|
|
2016
|
-
case "Refine": {
|
|
2017
|
-
const words = yield* ask(`refine:${round}`, {
|
|
2018
|
-
prompt: "What should change in the plan?",
|
|
2019
|
-
freeText: true,
|
|
2020
|
-
informationOnly: true,
|
|
2021
|
-
recommendationReason: "You chose to refine it, so only you know what should change."
|
|
2022
|
-
});
|
|
2023
|
-
const refined = yield* agentWorkOnce(`refine-plan:${round}`, {
|
|
2024
|
-
agent: "planner",
|
|
2025
|
-
prompt: refinePrompt(plan, words),
|
|
2026
|
-
output: Plan
|
|
2027
|
-
});
|
|
2028
|
-
const before = plan;
|
|
2029
|
-
revision++;
|
|
2030
|
-
({ plan, rejected } = yield* checked(refined, revision));
|
|
2031
|
-
yield* storePlan(plan, `:${round}`);
|
|
2032
|
-
yield* artifact(`changes:${round}`, {
|
|
2033
|
-
kind: "plan_changes",
|
|
2034
|
-
title: `Revision ${revision} of ${plan.title}`,
|
|
2035
|
-
text: changelog(before, plan, words)
|
|
2036
|
-
});
|
|
2037
|
-
break;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
if (filed?.revision !== revision)
|
|
2042
|
-
filed = {
|
|
2043
|
-
revision,
|
|
2044
|
-
tasks: (yield* createTasks("file-tasks:last", { tickets: plan.tickets, ...project })).tasks
|
|
2045
|
-
};
|
|
2046
|
-
return ended({
|
|
2047
|
-
implemented: false,
|
|
2048
|
-
stopped: `No decision after ${maxRounds} rounds, so the tickets are filed as Tasks for a person to pick up`
|
|
2049
|
-
});
|
|
2050
|
-
})
|
|
2051
|
-
});
|
|
2052
|
-
|
|
2053
|
-
// packages/loops/src/project-update.ts
|
|
2054
|
-
var Health = Literals(["on_track", "at_risk", "off_track"]);
|
|
2055
|
-
var Input7 = Struct({ projectId: String4 });
|
|
2056
|
-
var Output8 = Struct({
|
|
2057
|
-
draftId: optional(String4),
|
|
2058
|
-
health: optional(Health)
|
|
2059
|
-
});
|
|
2060
|
-
var Written = Struct({
|
|
2061
|
-
health: Health,
|
|
2062
|
-
note: String4,
|
|
2063
|
-
healthReason: optional(String4)
|
|
2064
|
-
});
|
|
2065
|
-
var label = { on_track: "on track", at_risk: "at risk", off_track: "off track" };
|
|
2066
|
-
var day = (iso) => new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
2067
|
-
var prompt2 = (read) => [
|
|
2068
|
-
`Write a Project Update for ${read.project.name} from what happened ${read.since === undefined ? "so far" : `since its last Update on ${day(read.since)}`}.`,
|
|
2069
|
-
"```json",
|
|
2070
|
-
JSON.stringify({ health: read.health, runs: read.runs, taskChanges: read.taskChanges }, null, 2),
|
|
2071
|
-
"```",
|
|
2072
|
-
"Say in a few short paragraphs what moved, what is stuck and what comes next, naming Tasks by key. Suggest a health: on_track, at_risk or off_track.",
|
|
2073
|
-
...read.health === undefined ? [] : [
|
|
2074
|
-
`The last Update said ${read.health}. If your health differs, give in healthReason one sentence on why, naming the Runs or Tasks that moved it.`
|
|
2075
|
-
],
|
|
2076
|
-
"A person reviews and posts it, so claim nothing the Runs and Task changes do not show."
|
|
2077
|
-
].join(`
|
|
2078
|
-
`);
|
|
2079
|
-
var noteOf = (read, written) => {
|
|
2080
|
-
if (read.health === undefined || read.health === written.health)
|
|
2081
|
-
return written.note;
|
|
2082
|
-
const moved = `Health moved from ${label[read.health]} to ${label[written.health]}`;
|
|
2083
|
-
return `${written.healthReason === undefined ? `${moved}.` : `${moved}: ${written.healthReason}`}
|
|
2084
|
-
|
|
2085
|
-
${written.note}`;
|
|
2086
|
-
};
|
|
2087
|
-
var project_update_default = defineLoop({
|
|
2088
|
-
id: "project-update",
|
|
2089
|
-
version: "2.0.1",
|
|
2090
|
-
title: "Project update",
|
|
2091
|
-
input: Input7,
|
|
2092
|
-
output: Output8,
|
|
2093
|
-
agents: { writer: "writer" },
|
|
2094
|
-
checkout: "none",
|
|
2095
|
-
capabilities: ["read_project", "agent_work", "draft_update"],
|
|
2096
|
-
run: ({ projectId }) => gen(function* () {
|
|
2097
|
-
const read = yield* projectActivity("read", { projectId });
|
|
2098
|
-
if (read.runs.length === 0 && read.taskChanges.length === 0)
|
|
2099
|
-
return {};
|
|
2100
|
-
const written = yield* agentWork("write", { agent: "writer", prompt: prompt2(read), output: Written });
|
|
2101
|
-
const draft = yield* draftUpdate("draft", {
|
|
2102
|
-
projectId,
|
|
2103
|
-
health: written.health,
|
|
2104
|
-
note: noteOf(read, written),
|
|
2105
|
-
writtenFrom: {
|
|
2106
|
-
runs: read.runs.length,
|
|
2107
|
-
taskChanges: read.taskChanges.length,
|
|
2108
|
-
...read.since === undefined ? undefined : { since: read.since }
|
|
2109
|
-
}
|
|
2110
|
-
});
|
|
2111
|
-
return { draftId: draft.id, health: written.health };
|
|
2112
|
-
})
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
// packages/loops/src/release.ts
|
|
2116
|
-
var Input8 = Struct({
|
|
2117
|
-
tag: optional(String4),
|
|
2118
|
-
revision: optional(String4),
|
|
2119
|
-
changeRequestId: optional(String4),
|
|
2120
|
-
deployTarget: optional(String4),
|
|
2121
|
-
deployEvent: optional(String4),
|
|
2122
|
-
deployTimeoutMinutes: optional(Int)
|
|
2123
|
-
});
|
|
2124
|
-
var Drafted = Struct({
|
|
2125
|
-
tag: String4,
|
|
2126
|
-
previousTag: optional(String4),
|
|
2127
|
-
title: String4,
|
|
2128
|
-
notes: String4,
|
|
2129
|
-
changeRequests: ArraySchema(String4)
|
|
2130
|
-
});
|
|
2131
|
-
var Output9 = Struct({
|
|
2132
|
-
status: Literals([
|
|
2133
|
-
"gate_failed",
|
|
2134
|
-
"not_mergeable",
|
|
2135
|
-
"rejected",
|
|
2136
|
-
"deployed",
|
|
2137
|
-
"rolled_back",
|
|
2138
|
-
"deploy_failed",
|
|
2139
|
-
"timed_out"
|
|
2140
|
-
]),
|
|
2141
|
-
reason: String4,
|
|
2142
|
-
tag: optional(String4),
|
|
2143
|
-
notesArtifactId: optional(String4),
|
|
2144
|
-
revision: optional(String4),
|
|
2145
|
-
url: optional(String4)
|
|
2146
|
-
});
|
|
2147
|
-
var draftPrompt = (tag, changeRequestId) => [
|
|
2148
|
-
"Draft the release notes for the ChangeRequests merged since the last release tag on the base branch, including those with no linked Task.",
|
|
2149
|
-
...changeRequestId === undefined ? [] : [`The release ChangeRequest ${changeRequestId} is part of it.`],
|
|
2150
|
-
"Group them by what a user of the software notices, and name each ChangeRequest you cover.",
|
|
2151
|
-
tag === undefined ? "Propose the next version tag, following the scheme earlier tags use. With no earlier tag, propose v0.1.0 and say so in the notes." : `The release is tagged ${tag}.`,
|
|
2152
|
-
"Give the latest earlier release tag as previousTag, or leave it out when there is none."
|
|
2153
|
-
].join(`
|
|
2154
|
-
|
|
2155
|
-
`);
|
|
2156
|
-
var redraftPrompt = (drafted, edit) => [
|
|
2157
|
-
"Redraft these release notes as asked, keeping the tag and everything not mentioned.",
|
|
2158
|
-
`Asked: ${edit}`,
|
|
2159
|
-
"```json",
|
|
2160
|
-
JSON.stringify(drafted, null, 2),
|
|
2161
|
-
"```"
|
|
2162
|
-
].join(`
|
|
2163
|
-
|
|
2164
|
-
`);
|
|
2165
|
-
var readyToMerge = (changeRequestId, round = 1) => gen(function* () {
|
|
2166
|
-
for (let pass = 1;; pass++) {
|
|
2167
|
-
const at = (name) => {
|
|
2168
|
-
const named = pass === 1 ? name : `${name}:moved:${pass}`;
|
|
2169
|
-
return round === 1 ? named : `${named}:${round}`;
|
|
2170
|
-
};
|
|
2171
|
-
let read = yield* pipelineStatus(at("gate"), { changeRequestId });
|
|
2172
|
-
for (let look = 1;look <= 10 && (read.status === "running" || read.status === "none"); look++) {
|
|
2173
|
-
const again = (name) => at(look === 1 ? name : `${name}:${look}`);
|
|
2174
|
-
yield* pipelineMoved(again("gate:pipeline"), changeRequestId, read);
|
|
2175
|
-
read = yield* pipelineStatus(again("gate:after"), { changeRequestId });
|
|
2176
|
-
}
|
|
2177
|
-
if (read.status !== "success") {
|
|
2178
|
-
const failing = read.jobs.filter((job) => job.status === "failed").map((job) => job.name);
|
|
2179
|
-
return {
|
|
2180
|
-
status: "gate_failed",
|
|
2181
|
-
reason: `The pipeline at ${read.head} is ${read.status}${failing.length === 0 ? "" : `: ${failing.join(", ")}`}`,
|
|
2182
|
-
revision: read.head
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2185
|
-
const approved = yield* waitFor(at("approved"), {
|
|
2186
|
-
event: "change_request.approval",
|
|
2187
|
-
filter: { changeRequestId, approved: true, head: read.head },
|
|
2188
|
-
timeout: "7 days",
|
|
2189
|
-
or: read.readAt === undefined ? [] : [{ event: "change_request.pipeline", filter: { changeRequestId }, since: read.readAt }]
|
|
2190
|
-
});
|
|
2191
|
-
if (approved.type !== "change_request.approval") {
|
|
2192
|
-
if (pass >= 10)
|
|
2193
|
-
return {
|
|
2194
|
-
status: "not_mergeable",
|
|
2195
|
-
reason: `ChangeRequest ${changeRequestId} kept moving while it awaited approval`
|
|
2196
|
-
};
|
|
2197
|
-
continue;
|
|
2198
|
-
}
|
|
2199
|
-
const mergeable = yield* waitFor(at("mergeable"), {
|
|
2200
|
-
event: "change_request.mergeable",
|
|
2201
|
-
filter: { changeRequestId },
|
|
2202
|
-
timeout: "1 hour"
|
|
2203
|
-
});
|
|
2204
|
-
if (mergeable.payload.mergeable === false)
|
|
2205
|
-
return {
|
|
2206
|
-
status: "not_mergeable",
|
|
2207
|
-
reason: `ChangeRequest ${changeRequestId} does not merge cleanly; bring it up to date first`
|
|
2208
|
-
};
|
|
2209
|
-
return { passed: read.head };
|
|
2210
|
-
}
|
|
2211
|
-
}).pipe(catchTags({
|
|
2212
|
-
WaitTimedOut: (timedOut) => succeed6({
|
|
2213
|
-
status: "not_mergeable",
|
|
2214
|
-
reason: `Nothing happened on ChangeRequest ${changeRequestId} while waiting for ${timedOut.event}`
|
|
2215
|
-
}),
|
|
2216
|
-
ChangeRequestClosed: (closed) => succeed6({
|
|
2217
|
-
status: "not_mergeable",
|
|
2218
|
-
reason: `ChangeRequest ${changeRequestId} was ${closed.merged ? "merged" : "closed"} before Kitman released it`
|
|
2219
|
-
})
|
|
2220
|
-
}));
|
|
2221
|
-
var recovery = (previous) => previous === undefined ? ["Retry the deploy", "Leave it"] : ["Retry the deploy", `Roll back to ${previous}`, "Leave it"];
|
|
2222
|
-
var release_default = defineLoop({
|
|
2223
|
-
id: "release",
|
|
2224
|
-
version: "2.0.0",
|
|
2225
|
-
title: "Release",
|
|
2226
|
-
input: Input8,
|
|
2227
|
-
output: Output9,
|
|
2228
|
-
agents: { releaser: "releaser" },
|
|
2229
|
-
checkout: "read",
|
|
2230
|
-
capabilities: [
|
|
2231
|
-
"agent_work",
|
|
2232
|
-
"ask",
|
|
2233
|
-
"verify",
|
|
2234
|
-
"claim",
|
|
2235
|
-
"publish_release",
|
|
2236
|
-
"read_change_request",
|
|
2237
|
-
"merge_change_request"
|
|
2238
|
-
],
|
|
2239
|
-
run: (input) => gen(function* () {
|
|
2240
|
-
const changeRequestId = input.changeRequestId;
|
|
2241
|
-
let gated;
|
|
2242
|
-
if (changeRequestId === undefined) {
|
|
2243
|
-
const gate = yield* verify("gate", input.revision === undefined ? {} : { revision: input.revision });
|
|
2244
|
-
if (!gate.passed) {
|
|
2245
|
-
const failing = gate.checks.filter((check) => check.status !== "passed").map((check) => check.name);
|
|
2246
|
-
return {
|
|
2247
|
-
status: "gate_failed",
|
|
2248
|
-
reason: `The checks fail at ${gate.revision}: ${failing.join(", ")}`,
|
|
2249
|
-
revision: gate.revision
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
gated = gate.revision;
|
|
2253
|
-
} else {
|
|
2254
|
-
const ready = yield* readyToMerge(changeRequestId);
|
|
2255
|
-
if (!("passed" in ready))
|
|
2256
|
-
return ready;
|
|
2257
|
-
gated = ready.passed;
|
|
2258
|
-
}
|
|
2259
|
-
let drafted = yield* agentWork("draft-notes", {
|
|
2260
|
-
agent: "releaser",
|
|
2261
|
-
prompt: draftPrompt(input.tag, changeRequestId),
|
|
2262
|
-
output: Drafted
|
|
2263
|
-
});
|
|
2264
|
-
const tag = input.tag ?? drafted.tag;
|
|
2265
|
-
for (let round = 1;; round++) {
|
|
2266
|
-
const answer = yield* ask(round === 1 ? "approve" : `approve:${round}`, {
|
|
2267
|
-
prompt: `Release ${tag} with these notes?
|
|
2268
|
-
|
|
2269
|
-
${drafted.notes}`,
|
|
2270
|
-
options: [
|
|
2271
|
-
{ value: "Approve", description: "Tags, publishes and deploys it." },
|
|
2272
|
-
{ value: "Edit notes", description: "Say what to change; the notes are redrafted." },
|
|
2273
|
-
{ value: "Reject", description: "Keeps the draft and releases nothing." }
|
|
2274
|
-
],
|
|
2275
|
-
recommended: "Approve",
|
|
2276
|
-
recommendationReason: "The release is proved: its checks pass, or its ChangeRequest is green, approved and mergeable.",
|
|
2277
|
-
default: null
|
|
2278
|
-
});
|
|
2279
|
-
if (answer === "Approve")
|
|
2280
|
-
break;
|
|
2281
|
-
if (answer === "Reject") {
|
|
2282
|
-
const why = yield* ask("reject-why", {
|
|
2283
|
-
prompt: `Why not release ${tag}? The draft is kept.`,
|
|
2284
|
-
freeText: true,
|
|
2285
|
-
informationOnly: true,
|
|
2286
|
-
recommendationReason: "Only you know why you rejected it.",
|
|
2287
|
-
default: "No reason given",
|
|
2288
|
-
timeout: "1 day"
|
|
2289
|
-
});
|
|
2290
|
-
const kept = yield* artifact("notes", {
|
|
2291
|
-
kind: "release_notes",
|
|
2292
|
-
title: drafted.title,
|
|
2293
|
-
text: drafted.notes,
|
|
2294
|
-
data: { tag, changeRequests: drafted.changeRequests }
|
|
2295
|
-
});
|
|
2296
|
-
return { status: "rejected", reason: why, tag, notesArtifactId: kept.artifactId };
|
|
2297
|
-
}
|
|
2298
|
-
const edit = yield* ask(`edit:${round}`, {
|
|
2299
|
-
prompt: "What should change in the notes?",
|
|
2300
|
-
freeText: true,
|
|
2301
|
-
informationOnly: true,
|
|
2302
|
-
recommendationReason: "You chose to edit them, so only you know what should change.",
|
|
2303
|
-
default: "",
|
|
2304
|
-
timeout: "1 day"
|
|
2305
|
-
});
|
|
2306
|
-
if (edit.trim() === "")
|
|
2307
|
-
continue;
|
|
2308
|
-
drafted = yield* agentWork(`redraft:${round}`, {
|
|
2309
|
-
agent: "releaser",
|
|
2310
|
-
prompt: redraftPrompt(drafted, edit),
|
|
2311
|
-
output: Drafted
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
const notes = yield* artifact("notes", {
|
|
2315
|
-
kind: "release_notes",
|
|
2316
|
-
title: drafted.title,
|
|
2317
|
-
text: drafted.notes,
|
|
2318
|
-
data: { tag, changeRequests: drafted.changeRequests }
|
|
2319
|
-
});
|
|
2320
|
-
let revision = gated;
|
|
2321
|
-
if (changeRequestId !== undefined) {
|
|
2322
|
-
for (let round = 1;; round++) {
|
|
2323
|
-
const merged = yield* mergeChangeRequest(round === 1 ? "merge" : `merge:${round}`, {
|
|
2324
|
-
changeRequestId,
|
|
2325
|
-
head: gated
|
|
2326
|
-
});
|
|
2327
|
-
if (merged.status === "conflict")
|
|
2328
|
-
return {
|
|
2329
|
-
status: "not_mergeable",
|
|
2330
|
-
reason: `ChangeRequest ${changeRequestId} no longer merges cleanly`,
|
|
2331
|
-
tag,
|
|
2332
|
-
notesArtifactId: notes.artifactId
|
|
2333
|
-
};
|
|
2334
|
-
if (merged.status === "merged") {
|
|
2335
|
-
revision = merged.revision;
|
|
2336
|
-
break;
|
|
2337
|
-
}
|
|
2338
|
-
if (round >= 3)
|
|
2339
|
-
return {
|
|
2340
|
-
status: "not_mergeable",
|
|
2341
|
-
reason: `ChangeRequest ${changeRequestId} kept moving while it was gated; it is at ${merged.head}`,
|
|
2342
|
-
tag,
|
|
2343
|
-
notesArtifactId: notes.artifactId
|
|
2344
|
-
};
|
|
2345
|
-
const ready = yield* readyToMerge(changeRequestId, round + 1);
|
|
2346
|
-
if (!("passed" in ready))
|
|
2347
|
-
return { ...ready, tag, notesArtifactId: notes.artifactId };
|
|
2348
|
-
gated = ready.passed;
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
const deployEvent = input.deployEvent ?? "deploy";
|
|
2352
|
-
yield* claim("deploy-target", { resource: `deploy:${input.deployTarget ?? deployEvent}` });
|
|
2353
|
-
const released = yield* release2("tag", {
|
|
2354
|
-
tag,
|
|
2355
|
-
revision,
|
|
2356
|
-
title: drafted.title,
|
|
2357
|
-
notes: drafted.notes
|
|
2358
|
-
});
|
|
2359
|
-
const deployed = (step, deployTag) => waitFor(step, {
|
|
2360
|
-
event: deployEvent,
|
|
2361
|
-
filter: { tag: deployTag },
|
|
2362
|
-
timeout: `${input.deployTimeoutMinutes ?? 60} minutes`
|
|
2363
|
-
}).pipe(map8((deploy) => deploy.payload.status === "failed" ? "deploy_failed" : "deployed"), catchTag("WaitTimedOut", () => succeed6("timed_out")));
|
|
2364
|
-
const settled = { tag, notesArtifactId: notes.artifactId, revision: released.revision, url: released.url };
|
|
2365
|
-
let status = yield* deployed("deploy", tag);
|
|
2366
|
-
for (let round = 1;status !== "deployed"; round++) {
|
|
2367
|
-
const options = recovery(drafted.previousTag);
|
|
2368
|
-
const answer = yield* ask(`recover:${round}`, {
|
|
2369
|
-
prompt: `${tag} ${status === "timed_out" ? "did not report a deploy in time" : "failed to deploy"}. What now?`,
|
|
2370
|
-
options,
|
|
2371
|
-
recommended: "Retry the deploy",
|
|
2372
|
-
recommendationReason: "A failed or late deploy is often passing, and a retry runs only the failed jobs of the same tag.",
|
|
2373
|
-
default: "Leave it",
|
|
2374
|
-
timeout: "1 hour"
|
|
2375
|
-
});
|
|
2376
|
-
if (answer === "Leave it")
|
|
2377
|
-
return { status, reason: `${tag} at ${released.revision} was left undeployed`, ...settled };
|
|
2378
|
-
if (answer === "Retry the deploy") {
|
|
2379
|
-
yield* rerunPipeline(`deploy:rerun-${round}`, { ref: tag });
|
|
2380
|
-
status = yield* deployed(`deploy:retry-${round}`, tag);
|
|
2381
|
-
continue;
|
|
2382
|
-
}
|
|
2383
|
-
const previous = drafted.previousTag ?? tag;
|
|
2384
|
-
yield* rerunPipeline(`deploy:redeploy-${round}`, { ref: previous, all: true });
|
|
2385
|
-
const back = yield* deployed(`deploy:rollback-${round}`, previous);
|
|
2386
|
-
if (back === "deployed")
|
|
2387
|
-
return { status: "rolled_back", reason: `Rolled back to ${previous}`, ...settled };
|
|
2388
|
-
status = back;
|
|
2389
|
-
}
|
|
2390
|
-
yield* artifact("report", {
|
|
2391
|
-
kind: "release_report",
|
|
2392
|
-
title: `${tag} deployed`,
|
|
2393
|
-
text: `${tag} at ${released.revision} deployed. ${released.url}`,
|
|
2394
|
-
data: { tag, status, revision: released.revision, url: released.url }
|
|
2395
|
-
});
|
|
2396
|
-
return { status: "deployed", reason: `${tag} deployed`, ...settled };
|
|
2397
|
-
})
|
|
2398
|
-
});
|
|
2399
|
-
|
|
2400
|
-
// packages/loops/src/research.ts
|
|
2401
|
-
class ResearchQuestionMissing extends TaggedError2()("ResearchQuestionMissing", {
|
|
2402
|
-
message: String4
|
|
2403
|
-
}) {
|
|
2404
|
-
}
|
|
2405
|
-
var Input9 = Struct({
|
|
2406
|
-
taskId: optional(String4),
|
|
2407
|
-
words: optional(String4),
|
|
2408
|
-
repoBindingId: optional(String4)
|
|
2409
|
-
});
|
|
2410
|
-
var Sources = NonEmptyArray(String4);
|
|
2411
|
-
var Fix = Struct({
|
|
2412
|
-
words: String4,
|
|
2413
|
-
outcome: LoopOutcome
|
|
2414
|
-
});
|
|
2415
|
-
var NextSteps = Struct({ tickets: PlanTickets });
|
|
2416
|
-
var Report = Struct({
|
|
2417
|
-
title: String4,
|
|
2418
|
-
summary: String4,
|
|
2419
|
-
findings: ArraySchema(Struct({
|
|
2420
|
-
finding: String4,
|
|
2421
|
-
basis: Literals(["observed", "read", "inferred"]),
|
|
2422
|
-
sources: Sources
|
|
2423
|
-
})),
|
|
2424
|
-
sources: Sources,
|
|
2425
|
-
unreached: ArraySchema(Struct({ source: String4, tried: String4 })),
|
|
2426
|
-
openQuestions: ArraySchema(String4),
|
|
2427
|
-
nextSteps: ArraySchema(String4),
|
|
2428
|
-
fix: optionalKey(Fix),
|
|
2429
|
-
nextStepTasks: optionalKey(NextSteps)
|
|
2430
|
-
});
|
|
2431
|
-
var Verdict = Struct({
|
|
2432
|
-
supported: Boolean2,
|
|
2433
|
-
gaps: ArraySchema(String4)
|
|
2434
|
-
});
|
|
2435
|
-
var Output10 = Struct({
|
|
2436
|
-
reportArtifactId: String4,
|
|
2437
|
-
gaps: ArraySchema(String4),
|
|
2438
|
-
unreached: ArraySchema(String4),
|
|
2439
|
-
fix: optionalKey(Fix),
|
|
2440
|
-
nextStepTasks: optionalKey(NextSteps)
|
|
2441
|
-
});
|
|
2442
|
-
var researchPrompt = (words) => [
|
|
2443
|
-
words === undefined ? "Research the question this Task asks, using the `research` skill. The Task's title, description, Acceptance Criteria and comments are in your context." : `Research this, using the \`research\` skill: ${words}`,
|
|
2444
|
-
"If your context has an earlier research report on this, extend it: keep what still holds, check what may have changed, and answer what it left open. Do not start over.",
|
|
2445
|
-
"Cite primary sources for every finding: the code, official documentation, specifications and first-party APIs. You may run things to reproduce behaviour; change nothing in the repository. Mark each finding observed (you ran it and saw it), read (a source says so) or inferred (you concluded it).",
|
|
2446
|
-
"List every source you needed and could not reach under `unreached`, with what you tried, and lower your conclusions to what the sources you did reach show.",
|
|
2447
|
-
"If the question turns out to be a bug report, say so in the summary and give `fix` with outcome bug. If the report proposes any other change, give `fix` with its outcome. Give next steps worth doing as `nextStepTasks` tickets too.",
|
|
2448
|
-
"Return the report as your Output. Do not write it into the repository."
|
|
2449
|
-
].join(`
|
|
2450
|
-
|
|
2451
|
-
`);
|
|
2452
|
-
var reviewPrompt = (report) => [
|
|
2453
|
-
"Judge whether this research report is supported: does each conclusion follow from the evidence it cites? Check the cited sources where you can.",
|
|
2454
|
-
"Answer supported, and list every gap: a claim its evidence does not show, including one that rests on a source listed as unreached. Do not ask for more scope; judge what it claims.",
|
|
2455
|
-
"```json",
|
|
2456
|
-
JSON.stringify(report, null, 2),
|
|
2457
|
-
"```"
|
|
2458
|
-
].join(`
|
|
2459
|
-
`);
|
|
2460
|
-
var againPrompt = (words, gaps) => [
|
|
2461
|
-
researchPrompt(words),
|
|
2462
|
-
"A reviewer found these claims unsupported by their evidence. Support each with evidence, or drop or weaken the claim:",
|
|
2463
|
-
...gaps.map((gap) => `- ${gap}`)
|
|
2464
|
-
].join(`
|
|
2465
|
-
|
|
2466
|
-
`);
|
|
2467
|
-
var list = (items) => items.length === 0 ? ["None."] : items.map((item) => `- ${item}`);
|
|
2468
|
-
var reportMarkdown = (report, gaps) => [
|
|
2469
|
-
`# ${report.title}`,
|
|
2470
|
-
"",
|
|
2471
|
-
report.summary,
|
|
2472
|
-
"",
|
|
2473
|
-
...gaps.length === 0 ? [] : ["## Gaps", "", "A reviewer found these unsupported:", "", ...list(gaps), ""],
|
|
2474
|
-
"## Findings",
|
|
2475
|
-
"",
|
|
2476
|
-
...report.findings.flatMap(({ finding, basis, sources }) => [
|
|
2477
|
-
`- ${finding} (${basis})`,
|
|
2478
|
-
...sources.map((source) => ` - ${source}`)
|
|
2479
|
-
]),
|
|
2480
|
-
"",
|
|
2481
|
-
"## Sources",
|
|
2482
|
-
"",
|
|
2483
|
-
...list(report.sources),
|
|
2484
|
-
"",
|
|
2485
|
-
...report.unreached.length === 0 ? [] : ["## Out of reach", "", ...report.unreached.map(({ source, tried }) => `- ${source}: ${tried}`), ""],
|
|
2486
|
-
"## Open questions",
|
|
2487
|
-
"",
|
|
2488
|
-
...list(report.openQuestions),
|
|
2489
|
-
"",
|
|
2490
|
-
"## Next steps",
|
|
2491
|
-
"",
|
|
2492
|
-
...list(report.nextSteps),
|
|
2493
|
-
...report.fix === undefined ? [] : ["", "## Proposed fix", "", `${report.fix.words} (${report.fix.outcome})`]
|
|
2494
|
-
].join(`
|
|
2495
|
-
`);
|
|
2496
|
-
var sample = {
|
|
2497
|
-
title: "Does the API cap request bodies?",
|
|
2498
|
-
summary: "It does, at 1 MB.",
|
|
2499
|
-
findings: [
|
|
2500
|
-
{
|
|
2501
|
-
finding: "The server sets maxRequestBodySize to 1 MB.",
|
|
2502
|
-
basis: "read",
|
|
2503
|
-
sources: ["apps/api/src/server.ts:20"]
|
|
2504
|
-
}
|
|
2505
|
-
],
|
|
2506
|
-
sources: ["apps/api/src/server.ts:20"],
|
|
2507
|
-
unreached: [],
|
|
2508
|
-
openQuestions: [],
|
|
2509
|
-
nextSteps: []
|
|
2510
|
-
};
|
|
2511
|
-
var research_default = defineLoop({
|
|
2512
|
-
id: "research",
|
|
2513
|
-
version: "2.0.0",
|
|
2514
|
-
title: "Research",
|
|
2515
|
-
input: Input9,
|
|
2516
|
-
output: Output10,
|
|
2517
|
-
outcome: "investigation",
|
|
2518
|
-
agents: { researcher: "researcher", reviewer: "reviewer" },
|
|
2519
|
-
checkout: "read",
|
|
2520
|
-
capabilities: ["agent_work"],
|
|
2521
|
-
offers: [
|
|
2522
|
-
{ id: "file-next-steps", title: "File next steps as Tasks", loop: "file-tasks", from: "nextStepTasks" },
|
|
2523
|
-
{ id: "build-fix", title: "Build the fix", loop: "build", from: "fix" }
|
|
2524
|
-
],
|
|
2525
|
-
checks: [
|
|
2526
|
-
{
|
|
2527
|
-
name: "reports",
|
|
2528
|
-
script: { input: { taskId: "task-1" }, outputs: { research: sample, review: { supported: true, gaps: [] } } }
|
|
2529
|
-
}
|
|
2530
|
-
],
|
|
2531
|
-
run: (input) => gen(function* () {
|
|
2532
|
-
if (input.taskId === undefined && input.words === undefined)
|
|
2533
|
-
return yield* new ResearchQuestionMissing({ message: "Give the question to research, or a Task" });
|
|
2534
|
-
const first = yield* agentWork("research", {
|
|
2535
|
-
agent: "researcher",
|
|
2536
|
-
prompt: researchPrompt(input.words),
|
|
2537
|
-
output: Report
|
|
2538
|
-
});
|
|
2539
|
-
const firstVerdict = yield* agentWork("review", {
|
|
2540
|
-
agent: "reviewer",
|
|
2541
|
-
prompt: reviewPrompt(first),
|
|
2542
|
-
output: Verdict
|
|
2543
|
-
});
|
|
2544
|
-
const report = firstVerdict.supported ? first : yield* agentWork("research:again", {
|
|
2545
|
-
agent: "researcher",
|
|
2546
|
-
prompt: againPrompt(input.words, firstVerdict.gaps),
|
|
2547
|
-
output: Report
|
|
2548
|
-
});
|
|
2549
|
-
const gaps = firstVerdict.supported ? [] : yield* agentWork("review:again", { agent: "reviewer", prompt: reviewPrompt(report), output: Verdict }).pipe(map8((verdict) => verdict.supported ? [] : verdict.gaps));
|
|
2550
|
-
const { nextStepTasks: _, ...data } = report;
|
|
2551
|
-
const { artifactId } = yield* artifact("report", {
|
|
2552
|
-
kind: "research_report",
|
|
2553
|
-
title: report.title,
|
|
2554
|
-
text: reportMarkdown(report, gaps),
|
|
2555
|
-
data
|
|
2556
|
-
});
|
|
2557
|
-
if (input.taskId !== undefined)
|
|
2558
|
-
yield* note("summary", { text: report.summary, artifactId });
|
|
2559
|
-
return {
|
|
2560
|
-
reportArtifactId: artifactId,
|
|
2561
|
-
gaps,
|
|
2562
|
-
unreached: report.unreached.map(({ source }) => source),
|
|
2563
|
-
...report.fix === undefined ? undefined : { fix: report.fix },
|
|
2564
|
-
...report.nextStepTasks === undefined ? undefined : { nextStepTasks: report.nextStepTasks }
|
|
2565
|
-
};
|
|
2566
|
-
})
|
|
2567
|
-
});
|
|
2568
|
-
|
|
2569
|
-
// packages/loops/src/hindsight.ts
|
|
2570
|
-
var Input10 = Struct({
|
|
2571
|
-
days: optional(Int.check(isGreaterThanOrEqualTo(1), isLessThanOrEqualTo(31))),
|
|
2572
|
-
teamId: optional(String4),
|
|
2573
|
-
improvements: optional(Int.check(isGreaterThanOrEqualTo(0), isLessThanOrEqualTo(5)))
|
|
2574
|
-
});
|
|
2575
|
-
var categories = [
|
|
2576
|
-
"navigation",
|
|
2577
|
-
"checks",
|
|
2578
|
-
"standards",
|
|
2579
|
-
"instructions",
|
|
2580
|
-
"tool_economy",
|
|
2581
|
-
"no_ops",
|
|
2582
|
-
"information_access",
|
|
2583
|
-
"kitman"
|
|
2584
|
-
];
|
|
2585
|
-
var Category = Literals(categories);
|
|
2586
|
-
var Finding2 = Struct({
|
|
2587
|
-
key: String4.check(isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), isMaxLength(80)),
|
|
2588
|
-
category: Category,
|
|
2589
|
-
kind: Literals(["memory", "task"]),
|
|
2590
|
-
lesson: String4,
|
|
2591
|
-
detail: optional(String4),
|
|
2592
|
-
fix: optional(Struct({ size: Literals(["errand", "build"]), words: String4 })),
|
|
2593
|
-
citations: ArraySchema(TranscriptPoint)
|
|
2594
|
-
});
|
|
2595
|
-
var Findings = Struct({ findings: ArraySchema(Finding2) });
|
|
2596
|
-
var Proposal = Struct({
|
|
2597
|
-
key: String4,
|
|
2598
|
-
category: Category,
|
|
2599
|
-
kind: Literals(["memory", "task"]),
|
|
2600
|
-
id: String4,
|
|
2601
|
-
matched: Boolean2,
|
|
2602
|
-
runIds: ArraySchema(String4)
|
|
2603
|
-
});
|
|
2604
|
-
var Output11 = Struct({
|
|
2605
|
-
summary: String4,
|
|
2606
|
-
proposals: ArraySchema(Proposal),
|
|
2607
|
-
leftOut: ArraySchema(Struct({ key: String4, reason: String4 })),
|
|
2608
|
-
improvements: ArraySchema(Struct({ key: String4, loop: Literals(["errand", "build"]), outcome: String4 }))
|
|
2609
|
-
});
|
|
2610
|
-
var prompt3 = (runs, days) => [
|
|
2611
|
-
`These Runs settled in the last ${days} days, each with its status: a failed one with its failure, one with a retried Step marked retried. Find what keeps going wrong in more than one Run: a failure, or in Runs that completed, a wasted step, a wrong turn, a check run again and again, or work redone. Fix the cause, not the symptom.`,
|
|
2612
|
-
"```json",
|
|
2613
|
-
JSON.stringify(runs, null, 2),
|
|
2614
|
-
"```",
|
|
2615
|
-
`Give each finding a category: ${categories.join(", ")}. Use kitman when Kitman itself misbehaved: a Run that stalled, parked or needed rescue for a reason in Kitman, not in the work.`,
|
|
2616
|
-
"Give each a stable kebab-case key naming the problem, not the week: the same problem must get the same key next time.",
|
|
2617
|
-
"Propose Memory when agents can avoid it by knowing something, or a Task when something has to change; when the change is clear, say how to fix it as an errand (small) or a build (a feature). Cite every Transcript point (runId, stepAttemptId, sequence) it shows in. Cite what happened: tool calls and results, files and answers. An agent's own messages are not evidence. Leave out anything seen in one Run only.",
|
|
2618
|
-
"Never quote a person's words into a lesson."
|
|
2619
|
-
].join(`
|
|
2620
|
-
|
|
2621
|
-
`);
|
|
2622
|
-
var point = (cited) => `${cited.runId}/${cited.stepAttemptId}/${cited.sequence}`;
|
|
2623
|
-
var shared = (cited, field) => {
|
|
2624
|
-
const values = new Set(cited.map((run) => run[field]));
|
|
2625
|
-
const [only] = values;
|
|
2626
|
-
return values.size === 1 ? only : undefined;
|
|
2627
|
-
};
|
|
2628
|
-
var proposal = (finding, runs) => {
|
|
2629
|
-
const evidence = new Set(runs.flatMap((run) => run.entries.flatMap((entry) => entry.kind === "message" ? [] : [point({ runId: run.runId, stepAttemptId: entry.stepAttemptId, sequence: entry.sequence })])));
|
|
2630
|
-
const citations = [
|
|
2631
|
-
...new Map(finding.citations.filter((cited) => evidence.has(point(cited))).map((cited) => [point(cited), cited])).values()
|
|
2632
|
-
];
|
|
2633
|
-
const runIds = new Set(citations.map((cited) => cited.runId));
|
|
2634
|
-
const [first, ...rest] = citations;
|
|
2635
|
-
if (first === undefined)
|
|
2636
|
-
return { reason: "It cites no evidence that was read: an agent\u2019s own words or a summary are not evidence" };
|
|
2637
|
-
if (runIds.size < 2)
|
|
2638
|
-
return { reason: "It was seen in one Run only" };
|
|
2639
|
-
const teamId = shared(runs.filter((run) => runIds.has(run.runId)), "teamId");
|
|
2640
|
-
if (finding.kind === "memory" && teamId !== undefined)
|
|
2641
|
-
return { request: { kind: "memory", teamId, content: finding.lesson, citations: [first, ...rest] } };
|
|
2642
|
-
return {
|
|
2643
|
-
request: {
|
|
2644
|
-
kind: "task",
|
|
2645
|
-
title: finding.lesson,
|
|
2646
|
-
description: finding.detail ?? finding.lesson,
|
|
2647
|
-
...teamId === undefined ? undefined : { teamId },
|
|
2648
|
-
key: finding.key,
|
|
2649
|
-
citations: [first, ...rest]
|
|
2650
|
-
}
|
|
2651
|
-
};
|
|
2652
|
-
};
|
|
2653
|
-
var hindsight_default = defineLoop({
|
|
2654
|
-
id: "hindsight",
|
|
2655
|
-
version: "1.0.0",
|
|
2656
|
-
title: "Hindsight",
|
|
2657
|
-
input: Input10,
|
|
2658
|
-
output: Output11,
|
|
2659
|
-
outcome: "investigation",
|
|
2660
|
-
agents: { investigator: "investigator" },
|
|
2661
|
-
checkout: "none",
|
|
2662
|
-
schedule: { rule: { frequency: "weekly", daysOfWeek: [1], hour: 9, minute: 0 }, timezone: "UTC" },
|
|
2663
|
-
capabilities: ["read_transcripts", "agent_work", "propose"],
|
|
2664
|
-
calls: () => [errand_default, build_default],
|
|
2665
|
-
run: (input) => gen(function* () {
|
|
2666
|
-
const days = input.days ?? 7;
|
|
2667
|
-
const read = yield* transcripts("read", { days });
|
|
2668
|
-
const runs = read.runs.filter((run) => input.teamId === undefined || run.teamId === input.teamId);
|
|
2669
|
-
if (runs.length < 2)
|
|
2670
|
-
return {
|
|
2671
|
-
summary: `Nothing recurred: ${runs.length === 0 ? "no Run" : "one Run"} settled in the last ${days} days.`,
|
|
2672
|
-
proposals: [],
|
|
2673
|
-
leftOut: [],
|
|
2674
|
-
improvements: []
|
|
2675
|
-
};
|
|
2676
|
-
const { findings } = yield* agentWork("find", {
|
|
2677
|
-
agent: "investigator",
|
|
2678
|
-
prompt: prompt3(runs, days),
|
|
2679
|
-
output: Findings
|
|
2680
|
-
});
|
|
2681
|
-
const judged = findings.map((finding) => ({ finding, judged: proposal(finding, runs) }));
|
|
2682
|
-
const leftOut = judged.flatMap(({ finding, judged }) => judged.request === undefined ? [{ key: finding.key, reason: judged.reason }] : []);
|
|
2683
|
-
const kept = judged.flatMap(({ finding, judged }) => judged.request === undefined ? [] : [{ finding, request: judged.request }]);
|
|
2684
|
-
const proposals = yield* forEach(kept, ({ finding, request }) => map8(propose(`propose:${finding.key}`, request), (made) => ({
|
|
2685
|
-
key: finding.key,
|
|
2686
|
-
category: finding.category,
|
|
2687
|
-
kind: made.kind,
|
|
2688
|
-
id: made.id,
|
|
2689
|
-
matched: made.matched === true,
|
|
2690
|
-
runIds: [...new Set(request.citations.map((cited) => cited.runId))]
|
|
2691
|
-
})));
|
|
2692
|
-
const fixable = kept.flatMap(({ finding }) => finding.fix === undefined ? [] : [{ ...finding, fix: finding.fix }]);
|
|
2693
|
-
const improvements = yield* forEach(fixable.slice(0, input.improvements ?? 0), (finding) => {
|
|
2694
|
-
const started = finding.fix.size === "errand" ? map8(call(`improve:${finding.key}`, errand_default, { words: finding.fix.words }), (done) => done.status) : map8(call(`improve:${finding.key}`, build_default, { words: finding.fix.words }), (built) => built.stopped);
|
|
2695
|
-
return started.pipe(catchTag("ChildFailed", (failed) => succeed6(`failed: ${failed.reason}`)), map8((outcome) => ({ key: finding.key, loop: finding.fix.size, outcome })));
|
|
2696
|
-
});
|
|
2697
|
-
const fresh = proposals.filter((made) => !made.matched).length;
|
|
2698
|
-
return {
|
|
2699
|
-
summary: proposals.length === 0 ? `Nothing recurred with evidence in ${runs.length} Runs.` : `${proposals.length} recurring ${proposals.length === 1 ? "problem" : "problems"}: ${fresh} proposed, ${proposals.length - fresh} added to earlier work.`,
|
|
2700
|
-
proposals,
|
|
2701
|
-
leftOut,
|
|
2702
|
-
improvements
|
|
2703
|
-
};
|
|
2704
|
-
})
|
|
2705
|
-
});
|
|
2706
|
-
|
|
2707
|
-
// packages/loops/src/sweep.ts
|
|
2708
|
-
var Input11 = Struct({
|
|
2709
|
-
authors: optional(NonEmptyArray(String4)),
|
|
2710
|
-
branch: optional(String4),
|
|
2711
|
-
repoBindingId: optional(String4),
|
|
2712
|
-
maxFixRounds: optional(Int.check(isGreaterThanOrEqualTo(0), isLessThanOrEqualTo(5)))
|
|
2713
|
-
});
|
|
2714
|
-
var Decision = Literals(["batch", "alone", "hold"]);
|
|
2715
|
-
var Assessed = Struct({
|
|
2716
|
-
updates: ArraySchema(Struct({
|
|
2717
|
-
number: String4,
|
|
2718
|
-
decision: Decision,
|
|
2719
|
-
reason: String4,
|
|
2720
|
-
major: Boolean2
|
|
2721
|
-
}))
|
|
2722
|
-
});
|
|
2723
|
-
var Batched = Struct({
|
|
2724
|
-
applied: ArraySchema(String4),
|
|
2725
|
-
dropped: ArraySchema(Struct({ number: String4, reason: String4 })),
|
|
2726
|
-
summary: String4
|
|
2727
|
-
});
|
|
2728
|
-
var Placed = Struct({ number: String4, title: String4, url: String4, reason: String4 });
|
|
2729
|
-
var Output12 = Struct({
|
|
2730
|
-
status: Literals(["nothing_to_sweep", "reported", "opened", "landed"]),
|
|
2731
|
-
summary: String4,
|
|
2732
|
-
landed: ArraySchema(Placed),
|
|
2733
|
-
left: ArraySchema(Placed),
|
|
2734
|
-
reportArtifactId: optional(String4),
|
|
2735
|
-
changeRequest: optional(ChangeRequestResult)
|
|
2736
|
-
});
|
|
2737
|
-
var defaultAuthors = ["renovate[bot]", "dependabot[bot]"];
|
|
2738
|
-
var listing = (requests) => requests.map((request) => [
|
|
2739
|
-
`### !${request.number} ${request.title}`,
|
|
2740
|
-
`Branch \`${request.sourceBranch}\` at ${request.head}, ${request.url}`,
|
|
2741
|
-
request.body
|
|
2742
|
-
].join(`
|
|
2743
|
-
|
|
2744
|
-
`)).join(`
|
|
2745
|
-
|
|
2746
|
-
`);
|
|
2747
|
-
var assessPrompt = (requests) => [
|
|
2748
|
-
"These dependency updates are open. Read each one\u2019s changelog and breaking-change notes, and what in this repository uses it.",
|
|
2749
|
-
listing(requests),
|
|
2750
|
-
"Decide for each: batch when it is safe to land with the others; alone when it needs its own review, such as a breaking change that needs code changes; hold when it should not land now, saying why.",
|
|
2751
|
-
"Say whether it bumps a major version. Change nothing."
|
|
2752
|
-
].join(`
|
|
2753
|
-
|
|
2754
|
-
`);
|
|
2755
|
-
var batchPrompt = (requests, target) => [
|
|
2756
|
-
`Bring these dependency updates onto this branch, which starts from ${target}: for each, merge \`origin/<branch>\` at the head named; Kitman fetched origin just before this Step, so do not fetch. Where lockfiles conflict, regenerate them with the package manager instead of hand-merging.`,
|
|
2757
|
-
listing(requests),
|
|
2758
|
-
"An update that cannot be brought in cleanly is dropped: leave it out and say why. Commit nothing yourself; Kitman pushes what you leave."
|
|
2759
|
-
].join(`
|
|
2760
|
-
|
|
2761
|
-
`);
|
|
2762
|
-
var fixPrompt3 = (failed, applied) => [
|
|
2763
|
-
`The batch of ${applied.map((number) => `!${number}`).join(", ")} fails these checks:`,
|
|
2764
|
-
...failed.checks.filter((check) => check.status === "failed").map((check) => `- ${check.name}: \`${check.command}\``),
|
|
2765
|
-
"Fix what an update broke, the way its changelog says to. If an update cannot be fixed in a small change, take it back out and drop it with the reason. Commit nothing yourself."
|
|
2766
|
-
].join(`
|
|
2767
|
-
|
|
2768
|
-
`);
|
|
2769
|
-
var supersedes = "Supersedes:";
|
|
2770
|
-
var supersededLine = (update) => `- !${update.number} ${update.title} (${update.url}) at ${update.head}`;
|
|
2771
|
-
var supersededIn = (body) => {
|
|
2772
|
-
const after = body.split(supersedes)[1] ?? "";
|
|
2773
|
-
const listed = after.split("Left for a person:")[0] ?? "";
|
|
2774
|
-
return listed.split(`
|
|
2775
|
-
`).flatMap((line) => {
|
|
2776
|
-
const found = /^- !(\S+) (.+) \((\S+)\) at (\S+)$/u.exec(line.trim());
|
|
2777
|
-
return found === null ? [] : [
|
|
2778
|
-
{
|
|
2779
|
-
number: found[1] ?? "",
|
|
2780
|
-
title: found[2] ?? "",
|
|
2781
|
-
url: found[3] ?? "",
|
|
2782
|
-
head: found[4] ?? "",
|
|
2783
|
-
reason: "Verified in the batch"
|
|
2784
|
-
}
|
|
2785
|
-
];
|
|
2786
|
-
});
|
|
2787
|
-
};
|
|
2788
|
-
var closeSuperseded = (landing, batchUrl) => map8(forEach(landing, ({ head, ...update }) => map8(closeChangeRequest(`close:${update.number}`, {
|
|
2789
|
-
number: update.number,
|
|
2790
|
-
head,
|
|
2791
|
-
note: `Landed in ${batchUrl} with the other dependency updates Sweep verified together.`
|
|
2792
|
-
}), (closed) => closed.head === undefined ? [] : [{ ...update, reason: `The bot pushed ${closed.head} after Sweep batched ${head}, so it stays open` }])), (moved) => moved.flat());
|
|
2793
|
-
var afterClosing = (landing, left, moved) => ({
|
|
2794
|
-
landed: landing.flatMap(({ head: _head, ...update }) => moved.some((open) => open.number === update.number) ? [] : [update]),
|
|
2795
|
-
left: [...left, ...moved]
|
|
2796
|
-
});
|
|
2797
|
-
var reportText = (landing, left, heading) => [
|
|
2798
|
-
heading,
|
|
2799
|
-
...landing.length === 0 ? [] : ["Landing:", ...landing.map((update) => `- !${update.number} ${update.title}`)],
|
|
2800
|
-
...left.length === 0 ? [] : ["Left for a person:", ...left.map((update) => `- !${update.number} ${update.title}: ${update.reason}`)]
|
|
2801
|
-
].join(`
|
|
2802
|
-
`);
|
|
2803
|
-
var sweep_default = defineLoop({
|
|
2804
|
-
id: "sweep",
|
|
2805
|
-
version: "1.0.0",
|
|
2806
|
-
title: "Sweep",
|
|
2807
|
-
input: Input11,
|
|
2808
|
-
output: Output12,
|
|
2809
|
-
agents: { assessor: "investigator", implementer: "implementer" },
|
|
2810
|
-
checkout: "write",
|
|
2811
|
-
schedule: { rule: { frequency: "weekly", daysOfWeek: [1], hour: 6, minute: 0 }, timezone: "UTC" },
|
|
2812
|
-
capabilities: ["agent_work", "verify", "change_request", "read_change_request", "merge_change_request"],
|
|
2813
|
-
calls: () => [shepherd_default],
|
|
2814
|
-
run: (input) => gen(function* () {
|
|
2815
|
-
const authors = input.authors ?? defaultAuthors;
|
|
2816
|
-
const [earlier] = (yield* ownChangeRequests("earlier")).changeRequests;
|
|
2817
|
-
if (earlier !== undefined) {
|
|
2818
|
-
const landed = yield* call("land", shepherd_default, {
|
|
2819
|
-
changeRequestId: earlier.id,
|
|
2820
|
-
merge: true,
|
|
2821
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
2822
|
-
});
|
|
2823
|
-
const summary = `${earlier.url}, from an earlier Sweep, is still open; Sweep took it on instead of batching again: ${landed.status}. ${landed.reason}`;
|
|
2824
|
-
const landing = supersededIn(earlier.body);
|
|
2825
|
-
const settled = afterClosing(landing, [], landed.status === "merged" ? yield* closeSuperseded(landing, earlier.url) : []);
|
|
2826
|
-
const stored = yield* artifact("report", {
|
|
2827
|
-
kind: "sweep_report",
|
|
2828
|
-
title: "Sweep",
|
|
2829
|
-
text: reportText(settled.landed, settled.left, summary)
|
|
2830
|
-
});
|
|
2831
|
-
return {
|
|
2832
|
-
status: landed.status === "merged" ? "landed" : "opened",
|
|
2833
|
-
summary,
|
|
2834
|
-
...settled,
|
|
2835
|
-
reportArtifactId: stored.artifactId,
|
|
2836
|
-
changeRequest: { id: earlier.id, number: earlier.number, url: earlier.url }
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
const open = (yield* openChangeRequests("bots", { authors })).changeRequests;
|
|
2840
|
-
if (open.length === 0)
|
|
2841
|
-
return {
|
|
2842
|
-
status: "nothing_to_sweep",
|
|
2843
|
-
summary: "The bots have nothing open.",
|
|
2844
|
-
landed: [],
|
|
2845
|
-
left: []
|
|
2846
|
-
};
|
|
2847
|
-
const assessed = yield* agentWork("assess", { agent: "assessor", prompt: assessPrompt(open), output: Assessed });
|
|
2848
|
-
const decisions = open.map((request) => {
|
|
2849
|
-
const decided = assessed.updates.find((update) => update.number === request.number);
|
|
2850
|
-
const placed = { number: request.number, title: request.title, url: request.url };
|
|
2851
|
-
if (decided === undefined)
|
|
2852
|
-
return { ...placed, decision: "hold", reason: "It was not assessed" };
|
|
2853
|
-
if (decided.major && decided.decision === "batch")
|
|
2854
|
-
return { ...placed, decision: "alone", reason: `A major version bump goes alone. ${decided.reason}` };
|
|
2855
|
-
return { ...placed, decision: decided.decision, reason: decided.reason };
|
|
2856
|
-
});
|
|
2857
|
-
const notBatched = decisions.flatMap(({ decision, ...placed }) => decision === "batch" ? [] : [placed]);
|
|
2858
|
-
const batch = decisions.filter((update) => update.decision === "batch");
|
|
2859
|
-
const reported = (status, summary, left) => map8(artifact("report", { kind: "sweep_report", title: "Sweep", text: reportText([], left, summary) }), (stored) => ({ status, summary, landed: [], left, reportArtifactId: stored.artifactId }));
|
|
2860
|
-
if (batch.length === 0)
|
|
2861
|
-
return yield* reported("reported", "Nothing is safe to batch.", notBatched);
|
|
2862
|
-
const now = (yield* openChangeRequests("bots:again", { authors })).changeRequests;
|
|
2863
|
-
const toBatch = now.filter((request) => batch.some((update) => update.number === request.number));
|
|
2864
|
-
const gone = batch.flatMap((update) => toBatch.some((request) => request.number === update.number) ? [] : [
|
|
2865
|
-
{
|
|
2866
|
-
number: update.number,
|
|
2867
|
-
title: update.title,
|
|
2868
|
-
url: update.url,
|
|
2869
|
-
reason: "It was closed or merged while Sweep ran"
|
|
2870
|
-
}
|
|
2871
|
-
]);
|
|
2872
|
-
const target = input.branch ?? "the default branch";
|
|
2873
|
-
let batched = yield* agentWork("batch", {
|
|
2874
|
-
agent: "implementer",
|
|
2875
|
-
prompt: batchPrompt(toBatch, target),
|
|
2876
|
-
output: Batched
|
|
2877
|
-
});
|
|
2878
|
-
let checked = yield* verify("verify", {});
|
|
2879
|
-
const maxFixRounds = input.maxFixRounds ?? 2;
|
|
2880
|
-
const dropped = new Map(batched.dropped.map((drop) => [drop.number, drop.reason]));
|
|
2881
|
-
for (let round = 1;!checked.passed && round <= maxFixRounds && batched.applied.length > 0; round++) {
|
|
2882
|
-
batched = yield* agentWork(`fix:${round}`, {
|
|
2883
|
-
agent: "implementer",
|
|
2884
|
-
prompt: fixPrompt3(checked, batched.applied),
|
|
2885
|
-
output: Batched
|
|
2886
|
-
});
|
|
2887
|
-
for (const drop of batched.dropped)
|
|
2888
|
-
dropped.set(drop.number, drop.reason);
|
|
2889
|
-
checked = yield* verify(`verify:${round}`, {});
|
|
2890
|
-
}
|
|
2891
|
-
const titled = (number) => toBatch.find((request) => request.number === number);
|
|
2892
|
-
const droppedOut = toBatch.flatMap((request) => {
|
|
2893
|
-
const reason = dropped.get(request.number) ?? (batched.applied.includes(request.number) ? undefined : "It was not brought onto the branch");
|
|
2894
|
-
return reason === undefined ? [] : [{ number: request.number, title: request.title, url: request.url, reason }];
|
|
2895
|
-
});
|
|
2896
|
-
const left = [...notBatched, ...gone, ...droppedOut];
|
|
2897
|
-
const landing = batched.applied.flatMap((number) => {
|
|
2898
|
-
const request = titled(number);
|
|
2899
|
-
return request === undefined ? [] : [{ number, title: request.title, url: request.url, head: request.head, reason: "Verified in the batch" }];
|
|
2900
|
-
});
|
|
2901
|
-
if (landing.length === 0)
|
|
2902
|
-
return yield* reported("reported", "No update survived the batch.", left);
|
|
2903
|
-
if (!checked.passed) {
|
|
2904
|
-
const failing = checked.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
2905
|
-
return yield* reported("reported", `The batch still fails ${failing.join(", ")} after ${maxFixRounds} rounds of fixing.`, [...left, ...landing.map(({ head: _head, ...update }) => ({ ...update, reason: "The batch did not pass" }))]);
|
|
2906
|
-
}
|
|
2907
|
-
const opened = yield* changeRequest("change-request", {
|
|
2908
|
-
title: `Update ${landing.length} ${landing.length === 1 ? "dependency" : "dependencies"}`,
|
|
2909
|
-
body: [
|
|
2910
|
-
batched.summary,
|
|
2911
|
-
supersedes,
|
|
2912
|
-
...landing.map(supersededLine),
|
|
2913
|
-
...left.length === 0 ? [] : ["Left for a person:", ...left.map((update) => `- !${update.number} ${update.title}: ${update.reason}`)]
|
|
2914
|
-
].join(`
|
|
2915
|
-
|
|
2916
|
-
`)
|
|
2917
|
-
});
|
|
2918
|
-
const landed = yield* call("land", shepherd_default, {
|
|
2919
|
-
changeRequestId: opened.id,
|
|
2920
|
-
merge: true,
|
|
2921
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
2922
|
-
});
|
|
2923
|
-
const merged = landed.status === "merged";
|
|
2924
|
-
const closed = afterClosing(landing, left, merged ? yield* closeSuperseded(landing, opened.url) : []);
|
|
2925
|
-
const stored = yield* artifact("report", {
|
|
2926
|
-
kind: "sweep_report",
|
|
2927
|
-
title: "Sweep",
|
|
2928
|
-
text: reportText(closed.landed, closed.left, `${opened.url}: ${landed.status}. ${landed.reason}`)
|
|
2929
|
-
});
|
|
2930
|
-
const settled = { ...closed, reportArtifactId: stored.artifactId, changeRequest: opened };
|
|
2931
|
-
if (!merged)
|
|
2932
|
-
return {
|
|
2933
|
-
status: "opened",
|
|
2934
|
-
summary: `${opened.url} is ${landed.status}: ${landed.reason}`,
|
|
2935
|
-
...settled
|
|
2936
|
-
};
|
|
2937
|
-
return {
|
|
2938
|
-
status: "landed",
|
|
2939
|
-
summary: `Landed ${closed.landed.length} in ${opened.url}`,
|
|
2940
|
-
...settled
|
|
2941
|
-
};
|
|
2942
|
-
})
|
|
2943
|
-
});
|
|
2944
|
-
|
|
2945
|
-
// packages/loops/src/tidy.ts
|
|
2946
|
-
var Input12 = Struct({
|
|
2947
|
-
revision: optional(String4),
|
|
2948
|
-
branch: optional(String4),
|
|
2949
|
-
repoBindingId: optional(String4),
|
|
2950
|
-
words: optional(String4)
|
|
2951
|
-
});
|
|
2952
|
-
var Hint = Literals(["architecture", "simplification"]);
|
|
2953
|
-
var Candidate = Struct({
|
|
2954
|
-
title: String4,
|
|
2955
|
-
words: String4,
|
|
2956
|
-
hint: Hint,
|
|
2957
|
-
worth: String4
|
|
2958
|
-
});
|
|
2959
|
-
var Survey = Struct({
|
|
2960
|
-
report: String4,
|
|
2961
|
-
htmlReport: optional(RunFile),
|
|
2962
|
-
candidates: ArraySchema(Candidate),
|
|
2963
|
-
questions: ArraySchema(Struct({
|
|
2964
|
-
question: String4,
|
|
2965
|
-
recommended: String4,
|
|
2966
|
-
why: String4
|
|
2967
|
-
})).check(isMaxLength(3))
|
|
2968
|
-
});
|
|
2969
|
-
var Tidied = Struct({
|
|
2970
|
-
improved: Boolean2,
|
|
2971
|
-
hint: optional(Hint),
|
|
2972
|
-
title: optional(String4),
|
|
2973
|
-
summary: String4,
|
|
2974
|
-
claims: ArraySchema(String4)
|
|
2975
|
-
});
|
|
2976
|
-
var Output13 = Struct({
|
|
2977
|
-
status: Literals(["nothing_worth_it", "reported", "planned", "opened"]),
|
|
2978
|
-
summary: String4,
|
|
2979
|
-
reportArtifactId: optional(String4),
|
|
2980
|
-
changeRequest: optional(ChangeRequestResult),
|
|
2981
|
-
settled: optional(Boolean2),
|
|
2982
|
-
unsettled: optional(ArraySchema(String4))
|
|
2983
|
-
});
|
|
2984
|
-
var hints = [
|
|
2985
|
-
"Hints, not a sequence; pick what is worth most:",
|
|
2986
|
-
"- architecture: deepen a shallow module, close a seam that leaks, or move behaviour to the data it uses; apply the deletion test (would deleting it make things simpler?).",
|
|
2987
|
-
"- simplification: duplication, dead code, needless indirection or abstraction."
|
|
2988
|
-
].join(`
|
|
2989
|
-
`);
|
|
2990
|
-
var surveyPrompt = (words, answers) => [
|
|
2991
|
-
`Survey ${words === undefined ? "the repository" : `this area: ${words}`} for the code-quality improvements worth a reviewer's time.`,
|
|
2992
|
-
"You run the improve-codebase-architecture skill as a Kitman Step. Write its HTML report, then keep it on the Run with attach_file and cite it as htmlReport instead of opening it. Stop after the report: Kitman asks the person which candidate to take, so do not grill here.",
|
|
2993
|
-
hints,
|
|
2994
|
-
"List the candidates most valuable first, and none when nothing is worth it; a small or cosmetic change is not worth it.",
|
|
2995
|
-
answers.length === 0 ? "Ask only what the code cannot tell you, such as intent or plans, and at most three questions. Keep each to the context and the question, and give the answer you would assume and why in one or two sentences." : `Your questions were answered:
|
|
2996
|
-
${answers.join(`
|
|
2997
|
-
`)}
|
|
2998
|
-
Ask nothing more.`,
|
|
2999
|
-
"Change nothing. Return a report in Markdown."
|
|
3000
|
-
].join(`
|
|
3001
|
-
|
|
3002
|
-
`);
|
|
3003
|
-
var tidyPrompt = (scope, words) => [
|
|
3004
|
-
`Make the one code-quality improvement worth most ${scope}.`,
|
|
3005
|
-
...words === undefined ? [] : [`Look at: ${words}`],
|
|
3006
|
-
hints,
|
|
3007
|
-
"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: that is a good outcome.",
|
|
3008
|
-
"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."
|
|
3009
|
-
].join(`
|
|
3010
|
-
|
|
3011
|
-
`);
|
|
3012
|
-
var areaOf = (revision, branch) => `in the area of the change at ${revision}: only the files that \`git diff $(git merge-base origin/${branch ?? "HEAD"} ${revision}) ${revision}\` touches, and the files that change with them`;
|
|
3013
|
-
var tidy_default = defineLoop({
|
|
3014
|
-
id: "tidy",
|
|
3015
|
-
version: "1.0.0",
|
|
3016
|
-
title: "Tidy",
|
|
3017
|
-
input: Input12,
|
|
3018
|
-
output: Output13,
|
|
3019
|
-
agents: { implementer: "implementer", surveyor: "surveyor" },
|
|
3020
|
-
checkout: "write",
|
|
3021
|
-
capabilities: ["agent_work", "ask", "verify", "change_request"],
|
|
3022
|
-
calls: () => [review_default, plan_default],
|
|
3023
|
-
run: (input) => gen(function* () {
|
|
3024
|
-
const start = input.revision ?? `origin/${input.branch ?? "HEAD"}`;
|
|
3025
|
-
const tidy = (scope, words) => gen(function* () {
|
|
3026
|
-
const improvement = yield* agentWork("tidy", {
|
|
3027
|
-
agent: "implementer",
|
|
3028
|
-
prompt: tidyPrompt(scope, words),
|
|
3029
|
-
output: Tidied
|
|
3030
|
-
});
|
|
3031
|
-
if (!improvement.improved)
|
|
3032
|
-
return { status: "nothing_worth_it", summary: improvement.summary };
|
|
3033
|
-
const base = yield* verify("verify:base", { revision: start });
|
|
3034
|
-
const head = yield* verify("verify", {});
|
|
3035
|
-
const reviewed = yield* call("review", review_default, {
|
|
3036
|
-
base: start,
|
|
3037
|
-
head: head.revision,
|
|
3038
|
-
outcome: "refactor",
|
|
3039
|
-
claims: improvement.claims
|
|
3040
|
-
});
|
|
3041
|
-
const settled = settleRound({ findings: reviewed.findings, rebuttals: [], verdicts: reviewed.verdicts });
|
|
3042
|
-
const decision = rallyStop({ round: 1, maxRounds: 1, settled });
|
|
3043
|
-
const verdict = evidenceGate({ outcome: "refactor", base, head });
|
|
3044
|
-
const unsettled = [
|
|
3045
|
-
...verdict.settled ? [] : verdict.reasons,
|
|
3046
|
-
...decision.stop ? unsettledItems(settled, decision) : []
|
|
3047
|
-
];
|
|
3048
|
-
const opened = yield* changeRequest("change-request", {
|
|
3049
|
-
title: improvement.title ?? "Tidy",
|
|
3050
|
-
body: changeRequestBody({
|
|
3051
|
-
summary: improvement.summary,
|
|
3052
|
-
head,
|
|
3053
|
-
claims: improvement.claims,
|
|
3054
|
-
unsettled,
|
|
3055
|
-
rounds: 1,
|
|
3056
|
-
stopped: decision.stop ? decision.reason : "round_cap"
|
|
3057
|
-
})
|
|
3058
|
-
});
|
|
3059
|
-
return {
|
|
3060
|
-
status: "opened",
|
|
3061
|
-
summary: improvement.summary,
|
|
3062
|
-
changeRequest: opened,
|
|
3063
|
-
settled: unsettled.length === 0,
|
|
3064
|
-
unsettled
|
|
3065
|
-
};
|
|
3066
|
-
});
|
|
3067
|
-
if (input.revision !== undefined)
|
|
3068
|
-
return yield* tidy(areaOf(input.revision, input.branch), input.words);
|
|
3069
|
-
let survey = yield* agentWork("survey", {
|
|
3070
|
-
agent: "surveyor",
|
|
3071
|
-
skill: "improve-codebase-architecture",
|
|
3072
|
-
prompt: surveyPrompt(input.words, []),
|
|
3073
|
-
output: Survey
|
|
3074
|
-
});
|
|
3075
|
-
if (survey.questions.length > 0) {
|
|
3076
|
-
const answers = yield* forEach(survey.questions, (question, index) => map8(ask(`question:${index + 1}`, {
|
|
3077
|
-
prompt: question.question,
|
|
3078
|
-
freeText: true,
|
|
3079
|
-
recommended: question.recommended,
|
|
3080
|
-
recommendationReason: question.why,
|
|
3081
|
-
default: "No answer; go by the code.",
|
|
3082
|
-
timeout: "1 day"
|
|
3083
|
-
}), (answer) => `- ${question.question} ${answer}`));
|
|
3084
|
-
survey = yield* agentWork("survey:answered", {
|
|
3085
|
-
agent: "surveyor",
|
|
3086
|
-
skill: "improve-codebase-architecture",
|
|
3087
|
-
prompt: surveyPrompt(input.words, answers),
|
|
3088
|
-
output: Survey
|
|
3089
|
-
});
|
|
3090
|
-
}
|
|
3091
|
-
const stored = yield* artifact("report", {
|
|
3092
|
-
kind: "tidy_report",
|
|
3093
|
-
title: "Tidy report",
|
|
3094
|
-
text: survey.report,
|
|
3095
|
-
...survey.htmlReport === undefined ? undefined : { files: [survey.htmlReport] }
|
|
3096
|
-
});
|
|
3097
|
-
const [top] = survey.candidates;
|
|
3098
|
-
if (top === undefined)
|
|
3099
|
-
return {
|
|
3100
|
-
status: "nothing_worth_it",
|
|
3101
|
-
summary: "Nothing here is worth a reviewer\u2019s time.",
|
|
3102
|
-
reportArtifactId: stored.artifactId
|
|
3103
|
-
};
|
|
3104
|
-
const answer = yield* ask("next", {
|
|
3105
|
-
prompt: `Tidy found ${survey.candidates.length} worth doing, top: ${top.title}. ${top.worth}
|
|
3106
|
-
|
|
3107
|
-
What next?`,
|
|
3108
|
-
options: [
|
|
3109
|
-
{ value: "Plan it", description: "Plan files the candidates as Tasks." },
|
|
3110
|
-
{ value: "Tidy the top one now", description: "An agent makes the top one and opens a ChangeRequest." },
|
|
3111
|
-
{ value: "Done", description: "Keep the report and stop." }
|
|
3112
|
-
],
|
|
3113
|
-
recommended: "Tidy the top one now",
|
|
3114
|
-
recommendationReason: `The top one is worth a reviewer's time on its own: ${top.worth}`,
|
|
3115
|
-
default: "Done",
|
|
3116
|
-
timeout: "1 day"
|
|
3117
|
-
});
|
|
3118
|
-
const reported = { summary: `Top: ${top.title}`, reportArtifactId: stored.artifactId };
|
|
3119
|
-
if (answer === "Done")
|
|
3120
|
-
return { status: "reported", ...reported };
|
|
3121
|
-
if (answer === "Plan it") {
|
|
3122
|
-
yield* call("plan", plan_default, {
|
|
3123
|
-
words: [
|
|
3124
|
-
"Plan these code-quality improvements:",
|
|
3125
|
-
...survey.candidates.map((candidate) => `- ${candidate.title}: ${candidate.words}`),
|
|
3126
|
-
survey.report
|
|
3127
|
-
].join(`
|
|
3128
|
-
|
|
3129
|
-
`),
|
|
3130
|
-
...input.repoBindingId === undefined ? undefined : { repoBindingId: input.repoBindingId },
|
|
3131
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
3132
|
-
});
|
|
3133
|
-
return { status: "planned", ...reported };
|
|
3134
|
-
}
|
|
3135
|
-
const synced = yield* syncBase("sync", {
|
|
3136
|
-
agent: "implementer",
|
|
3137
|
-
...input.branch === undefined ? undefined : { target: input.branch }
|
|
3138
|
-
});
|
|
3139
|
-
if (synced.status === "conflict")
|
|
3140
|
-
return {
|
|
3141
|
-
status: "reported",
|
|
3142
|
-
...reported,
|
|
3143
|
-
summary: `Top: ${top.title}. Bringing the branch up to date conflicts on ${synced.files.join(", ")}`
|
|
3144
|
-
};
|
|
3145
|
-
const tidied = yield* tidy("", `${top.title}: ${top.words}`);
|
|
3146
|
-
return { ...tidied, reportArtifactId: stored.artifactId };
|
|
3147
|
-
})
|
|
3148
|
-
});
|
|
3149
|
-
|
|
3150
|
-
// packages/loops/src/index.ts
|
|
3151
|
-
var shippedLoops = {
|
|
3152
|
-
build: build_default,
|
|
3153
|
-
errand: errand_default,
|
|
3154
|
-
"file-tasks": file_tasks_default,
|
|
3155
|
-
hillclimb: hillclimb_default,
|
|
3156
|
-
firewatch: firewatch_default,
|
|
3157
|
-
plan: plan_default,
|
|
3158
|
-
"project-update": project_update_default,
|
|
3159
|
-
release: release_default,
|
|
3160
|
-
research: research_default,
|
|
3161
|
-
hindsight: hindsight_default,
|
|
3162
|
-
review: review_default,
|
|
3163
|
-
shepherd: shepherd_default,
|
|
3164
|
-
sweep: sweep_default,
|
|
3165
|
-
tidy: tidy_default
|
|
3166
|
-
};
|
|
3167
|
-
export {
|
|
3168
|
-
build_default as build,
|
|
3169
|
-
errand_default as errand,
|
|
3170
|
-
file_tasks_default as fileTasks,
|
|
3171
|
-
firewatch_default as firewatch,
|
|
3172
|
-
hillclimb_default as hillclimb,
|
|
3173
|
-
hindsight_default as hindsight,
|
|
3174
|
-
plan_default as plan,
|
|
3175
|
-
project_update_default as projectUpdate,
|
|
3176
|
-
release_default as release,
|
|
3177
|
-
research_default as research,
|
|
3178
|
-
review_default as review,
|
|
3179
|
-
shepherd_default as shepherd,
|
|
3180
|
-
shippedLoops,
|
|
3181
|
-
sweep_default as sweep,
|
|
3182
|
-
tidy_default as tidy
|
|
3183
|
-
};
|