@kitman-sh/software-engineering 0.10.57 → 0.10.59
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/agents/deployer.md +11 -0
- package/dist/bundle.js +1934 -1836
- package/dist/declarations.json +109 -11
- package/kitman.config.ts +5 -0
- package/package.json +3 -3
- package/src/build.ts +13 -1
- package/src/plan.ts +3 -1
- package/src/release.ts +51 -2
- package/src/review.ts +30 -9
- package/src/shepherd-output.ts +5 -1
- package/src/shepherd.ts +40 -2
package/dist/bundle.js
CHANGED
|
@@ -13996,7 +13996,7 @@ var StepOutputSubmissionResult = Union2([
|
|
|
13996
13996
|
}),
|
|
13997
13997
|
Struct({ status: Literal2("failed"), message: String5 })
|
|
13998
13998
|
]);
|
|
13999
|
-
var HarnessHoldReasons = ["out_of_quota", "failed_to_start"];
|
|
13999
|
+
var HarnessHoldReasons = ["out_of_quota", "rate_limited", "login_refused", "failed_to_start"];
|
|
14000
14000
|
var SkippedChoiceReasons = ["not_on_runner", ...HarnessHoldReasons];
|
|
14001
14001
|
var SkippedChoice = Struct({
|
|
14002
14002
|
harness: optionalKey2(String5),
|
|
@@ -19011,12 +19011,15 @@ var ask = (name, question) => takeStep(name, "ask", "ask", gen2(function* () {
|
|
|
19011
19011
|
const fallback = question.default === undefined ? recommendedDefault(recommended) : question.default ?? undefined;
|
|
19012
19012
|
if (fallback !== undefined && options !== undefined && !options.includes(fallback))
|
|
19013
19013
|
return yield* new QuestionRefused({ step: name, answer: fallback, options });
|
|
19014
|
+
if (question.auto !== undefined && options !== undefined && !options.includes(question.auto))
|
|
19015
|
+
return yield* new QuestionRefused({ step: name, answer: question.auto, options });
|
|
19014
19016
|
yield* launch(deferred, (host, token2) => host.putQuestion({
|
|
19015
19017
|
step: name,
|
|
19016
19018
|
prompt: question.prompt,
|
|
19017
19019
|
...options === undefined ? undefined : { options },
|
|
19018
19020
|
...advice,
|
|
19019
19021
|
...fallback === undefined ? undefined : { default: fallback },
|
|
19022
|
+
...question.auto === undefined ? undefined : { auto: question.auto },
|
|
19020
19023
|
...question.timeout === undefined ? undefined : { timeoutMillis: toMillis(fromInputUnsafe2(question.timeout)) },
|
|
19021
19024
|
token: token2
|
|
19022
19025
|
}));
|
|
@@ -20257,7 +20260,7 @@ var scriptedHost = (script, engine, agentLaunches, childLaunches, waits) => {
|
|
|
20257
20260
|
...launch.mergeTarget === undefined ? undefined : { mergeTarget: launch.mergeTarget }
|
|
20258
20261
|
})), resolve(agentDeferredNamed(launch.deferredName), launch.token, scriptedExit(script.outputs?.[launch.step], (reason) => new StepFailed({ step: launch.step, reason })))),
|
|
20259
20262
|
putQuestion: (launch) => {
|
|
20260
|
-
const answer = script.answers?.[launch.step] ?? launch.default;
|
|
20263
|
+
const answer = script.answers?.[launch.step] ?? (script.fullAuto === true ? launch.auto : undefined) ?? launch.default;
|
|
20261
20264
|
return resolve(answerDeferred(launch.step), launch.token, answer === undefined ? fail5(new StepFailed({ step: launch.step, reason: "no answer was scripted" })) : succeed4(answer));
|
|
20262
20265
|
},
|
|
20263
20266
|
admitChild: (launch) => andThen2(sync2(() => childLaunches.push({ step: launch.step, loop: launch.loop, input: launch.input })), resolve(childDeferred(launch.step), launch.token, scriptedExit(script.children?.[launch.step], (reason) => new ChildFailed({ step: launch.step, loop: launch.loop, reason })))),
|
|
@@ -20763,18 +20766,27 @@ var review_default = defineLoop({
|
|
|
20763
20766
|
}).pipe(map8((output) => [seat, output])), { concurrency: "unbounded" });
|
|
20764
20767
|
const earlier = input.earlier ?? [];
|
|
20765
20768
|
const earlierIds = new Set(earlier.map((finding) => finding.id));
|
|
20766
|
-
const
|
|
20767
|
-
const standing = new Set(
|
|
20768
|
-
const
|
|
20769
|
+
const answered = followUpsOf(earlier, outputs.flatMap(([, output]) => output.followUps ?? []));
|
|
20770
|
+
const standing = new Set(answered.flatMap((entry) => entry.status === "fixed" ? [] : [entry.id]));
|
|
20771
|
+
const verdicts = outputs.flatMap(([, output]) => output.verdicts ?? []);
|
|
20772
|
+
const rebutted = new Set((input.rebuttals ?? []).map((rebuttal) => rebuttal.findingId));
|
|
20773
|
+
const withdrawn = (id) => {
|
|
20774
|
+
const on = verdicts.filter((verdict) => verdict.findingId === id);
|
|
20775
|
+
return rebutted.has(id) && on.length > 0 && on.every((verdict) => verdict.accepted);
|
|
20776
|
+
};
|
|
20777
|
+
const carried = earlier.filter((finding) => standing.has(finding.id) && !withdrawn(finding.id));
|
|
20778
|
+
const followUps = answered.filter((entry) => !withdrawn(entry.id));
|
|
20769
20779
|
const fresh = mergeFindings(Object.fromEntries(outputs.map(([seat, output]) => [
|
|
20770
20780
|
seat,
|
|
20771
20781
|
output.findings.filter((finding) => !earlierIds.has(`${finding.file}:${finding.line}`))
|
|
20772
20782
|
])));
|
|
20773
20783
|
const findings = [...carried, ...fresh];
|
|
20774
|
-
const
|
|
20784
|
+
const blockers = findings.filter(isBlocking);
|
|
20785
|
+
const scope = input.files === undefined ? outputs.find(([seat]) => seat === scopeSeat)?.[1].scope : undefined;
|
|
20786
|
+
const failing = input.failing ?? [];
|
|
20787
|
+
const summary = blockers.length > 0 ? `Blocking: fix ${blockers.map((finding) => finding.id).join(", ")} before merging.` : scope?.met === false ? "Not ready: the change does not yet do what was asked." : failing.length > 0 ? `Not ready: ${failing.join(", ")} fails on this head.` : findings.length === 0 ? "No findings: nothing stands in the way of merging." : ["Can merge: nothing blocks it.", ...outputs.map(([, output]) => output.summary)].join(" ");
|
|
20775
20788
|
const freshIds = new Set(fresh.map((finding) => finding.id));
|
|
20776
20789
|
const text = reviewText(summary, findings, followUps);
|
|
20777
|
-
const scope = input.files === undefined ? outputs.find(([seat]) => seat === scopeSeat)?.[1].scope : undefined;
|
|
20778
20790
|
const stored = yield* artifact("findings", {
|
|
20779
20791
|
kind: "review-findings",
|
|
20780
20792
|
title: "Review findings",
|
|
@@ -20785,7 +20797,7 @@ var review_default = defineLoop({
|
|
|
20785
20797
|
summary,
|
|
20786
20798
|
findings,
|
|
20787
20799
|
followUps,
|
|
20788
|
-
verdicts
|
|
20800
|
+
verdicts,
|
|
20789
20801
|
blocking: findings.some(isBlocking),
|
|
20790
20802
|
artifactId: stored.artifactId,
|
|
20791
20803
|
...scope === undefined ? undefined : { scope }
|
|
@@ -20793,7 +20805,7 @@ var review_default = defineLoop({
|
|
|
20793
20805
|
const changeRequestId = input.changeRequestId;
|
|
20794
20806
|
if (changeRequestId === undefined || head === undefined)
|
|
20795
20807
|
return reviewed;
|
|
20796
|
-
const approval = reviewed.blocking || scope?.met === false ||
|
|
20808
|
+
const approval = reviewed.blocking || scope?.met === false || failing.length > 0 ? undefined : yield* approveChangeRequest("approve", { changeRequestId, head, summary }).pipe(map8((result) => result.approved ? { head, approved: true } : { head, approved: false, reason: result.reason }));
|
|
20797
20809
|
const answer = input.next ?? (yield* ask("next", {
|
|
20798
20810
|
prompt: `${summary}
|
|
20799
20811
|
|
|
@@ -20836,2010 +20848,2088 @@ What next?`,
|
|
|
20836
20848
|
})
|
|
20837
20849
|
});
|
|
20838
20850
|
|
|
20839
|
-
// src/
|
|
20840
|
-
var ShepherdOutput = Struct({
|
|
20841
|
-
status: Literals(["merged", "ready", "needs_you"]),
|
|
20842
|
-
reason: String5,
|
|
20843
|
-
fixRounds: Int,
|
|
20844
|
-
replies: Int
|
|
20845
|
-
});
|
|
20846
|
-
|
|
20847
|
-
// src/shepherd.ts
|
|
20848
|
-
var reviewLoop = () => ({ id: "review", input: Input, output: Output });
|
|
20849
|
-
var stepBudget = "45 minutes";
|
|
20851
|
+
// src/release.ts
|
|
20850
20852
|
var Input2 = Struct({
|
|
20851
|
-
|
|
20852
|
-
|
|
20853
|
-
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
maxCycles: optional2(Int)
|
|
20853
|
+
tag: optional2(String5),
|
|
20854
|
+
revision: optional2(String5),
|
|
20855
|
+
changeRequestId: optional2(String5),
|
|
20856
|
+
deployTarget: optional2(String5),
|
|
20857
|
+
deployEvent: optional2(String5),
|
|
20858
|
+
deployTimeoutMinutes: optional2(Int)
|
|
20858
20859
|
});
|
|
20859
|
-
var
|
|
20860
|
-
|
|
20861
|
-
|
|
20862
|
-
|
|
20860
|
+
var Drafted = Struct({
|
|
20861
|
+
tag: String5,
|
|
20862
|
+
previousTag: optional2(String5),
|
|
20863
|
+
title: String5,
|
|
20864
|
+
notes: String5,
|
|
20865
|
+
changeRequests: ArraySchema(String5)
|
|
20863
20866
|
});
|
|
20864
|
-
var
|
|
20865
|
-
|
|
20866
|
-
|
|
20867
|
-
reply: String5,
|
|
20868
|
-
changed: Boolean3
|
|
20869
|
-
}))
|
|
20867
|
+
var Deployed = Struct({
|
|
20868
|
+
status: Literals(["deployed", "failed", "rolled_back", "rollback_failed", "none"]),
|
|
20869
|
+
detail: String5
|
|
20870
20870
|
});
|
|
20871
|
-
var
|
|
20872
|
-
|
|
20873
|
-
${
|
|
20874
|
-
|
|
20875
|
-
`
|
|
20876
|
-
|
|
20877
|
-
"Run the failing job once more here to rule out a flake; if it passes, change nothing and answer flaky.",
|
|
20878
|
-
`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.`,
|
|
20879
|
-
"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."
|
|
20871
|
+
var deployPrompt = (tag, previousTag) => [
|
|
20872
|
+
`Deploy the release ${tag}, which Kitman has just tagged and pushed.`,
|
|
20873
|
+
`If the repository has no executable \`.kitman/deploy\` at ${tag}, change nothing and answer status none.`,
|
|
20874
|
+
`Otherwise run \`.kitman/deploy ${tag}${previousTag === undefined ? "" : ` ${previousTag}`}\` from the checkout of ${tag} (\`git fetch --tags\` first), and do nothing else: it backs up, publishes, deploys, verifies and rolls back by itself, and may take an hour. Run it in the background with its output in a log file, and wait for it to exit, checking every few minutes; never start it twice.`,
|
|
20875
|
+
"Answer with the status its last line `kitman-deploy: <status> <detail>` names, and that detail. If it exited without that line, answer failed with its exit code and the end of its log.",
|
|
20876
|
+
"Never print, copy or summarise the variables or secrets it pulls."
|
|
20880
20877
|
].join(`
|
|
20881
20878
|
|
|
20882
20879
|
`);
|
|
20883
|
-
var
|
|
20884
|
-
|
|
20885
|
-
|
|
20886
|
-
|
|
20887
|
-
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20880
|
+
var Output2 = Struct({
|
|
20881
|
+
status: Literals([
|
|
20882
|
+
"gate_failed",
|
|
20883
|
+
"not_mergeable",
|
|
20884
|
+
"rejected",
|
|
20885
|
+
"deployed",
|
|
20886
|
+
"rolled_back",
|
|
20887
|
+
"deploy_failed",
|
|
20888
|
+
"rollback_failed",
|
|
20889
|
+
"timed_out"
|
|
20890
|
+
]),
|
|
20891
|
+
reason: String5,
|
|
20892
|
+
tag: optional2(String5),
|
|
20893
|
+
notesArtifactId: optional2(String5),
|
|
20894
|
+
revision: optional2(String5),
|
|
20895
|
+
url: optional2(String5)
|
|
20896
|
+
});
|
|
20897
|
+
var draftPrompt = (tag, changeRequestId) => [
|
|
20898
|
+
"Draft the release notes for the ChangeRequests merged since the last release tag on the base branch, including those with no linked Task.",
|
|
20899
|
+
...changeRequestId === undefined ? [] : [`The release ChangeRequest ${changeRequestId} is part of it.`],
|
|
20900
|
+
"Group them by what a user of the software notices, and name each ChangeRequest you cover.",
|
|
20901
|
+
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}.`,
|
|
20902
|
+
"Give the latest earlier release tag as previousTag, or leave it out when there is none."
|
|
20892
20903
|
].join(`
|
|
20904
|
+
|
|
20893
20905
|
`);
|
|
20894
|
-
var
|
|
20895
|
-
|
|
20896
|
-
|
|
20897
|
-
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
return { kind: "changed", why: `the branch's own diff changed in ${elsewhere.join(", ")}` };
|
|
20901
|
-
return files.length === 0 ? { kind: "carry" } : { kind: "review", files };
|
|
20902
|
-
};
|
|
20903
|
-
var repliesPrompt = (threads) => [
|
|
20904
|
-
"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.",
|
|
20905
|
-
"Never mark a thread resolved: whether a disputed point is settled is the reviewer's call.",
|
|
20906
|
-
...threads.map(threadLine)
|
|
20906
|
+
var redraftPrompt = (drafted, edit) => [
|
|
20907
|
+
"Redraft these release notes as asked, keeping the tag and everything not mentioned.",
|
|
20908
|
+
`Asked: ${edit}`,
|
|
20909
|
+
"```json",
|
|
20910
|
+
JSON.stringify(drafted, null, 2),
|
|
20911
|
+
"```"
|
|
20907
20912
|
].join(`
|
|
20908
20913
|
|
|
20909
20914
|
`);
|
|
20910
|
-
var
|
|
20911
|
-
|
|
20912
|
-
|
|
20913
|
-
|
|
20914
|
-
|
|
20915
|
+
var readyToMerge = (changeRequestId, round = 1) => gen2(function* () {
|
|
20916
|
+
for (let pass = 1;; pass++) {
|
|
20917
|
+
const at = (name) => {
|
|
20918
|
+
const named = pass === 1 ? name : `${name}:moved:${pass}`;
|
|
20919
|
+
return round === 1 ? named : `${named}:${round}`;
|
|
20920
|
+
};
|
|
20921
|
+
let read = yield* pipelineStatus(at("gate"), { changeRequestId });
|
|
20922
|
+
for (let look = 1;look <= 10 && (read.status === "running" || read.status === "none"); look++) {
|
|
20923
|
+
const again = (name) => at(look === 1 ? name : `${name}:${look}`);
|
|
20924
|
+
yield* pipelineMoved(again("gate:pipeline"), changeRequestId, read);
|
|
20925
|
+
read = yield* pipelineStatus(again("gate:after"), { changeRequestId });
|
|
20926
|
+
}
|
|
20927
|
+
if (read.status !== "success") {
|
|
20928
|
+
const failing = read.jobs.filter((job) => job.status === "failed").map((job) => job.name);
|
|
20929
|
+
return {
|
|
20930
|
+
status: "gate_failed",
|
|
20931
|
+
reason: `The pipeline at ${read.head} is ${read.status}${failing.length === 0 ? "" : `: ${failing.join(", ")}`}`,
|
|
20932
|
+
revision: read.head
|
|
20933
|
+
};
|
|
20934
|
+
}
|
|
20935
|
+
const approved = yield* waitFor(at("approved"), {
|
|
20936
|
+
event: "change_request.approval",
|
|
20937
|
+
filter: { changeRequestId, approved: true, head: read.head },
|
|
20938
|
+
timeout: "7 days",
|
|
20939
|
+
or: read.readAt === undefined ? [] : [{ event: "change_request.pipeline", filter: { changeRequestId }, since: read.readAt }]
|
|
20940
|
+
});
|
|
20941
|
+
if (approved.type !== "change_request.approval") {
|
|
20942
|
+
if (pass >= 10)
|
|
20943
|
+
return {
|
|
20944
|
+
status: "not_mergeable",
|
|
20945
|
+
reason: `ChangeRequest ${changeRequestId} kept moving while it awaited approval`
|
|
20946
|
+
};
|
|
20947
|
+
continue;
|
|
20948
|
+
}
|
|
20949
|
+
const mergeable = yield* waitFor(at("mergeable"), {
|
|
20950
|
+
event: "change_request.mergeable",
|
|
20951
|
+
filter: { changeRequestId },
|
|
20952
|
+
timeout: "1 hour"
|
|
20953
|
+
});
|
|
20954
|
+
if (mergeable.payload.mergeable === false)
|
|
20955
|
+
return {
|
|
20956
|
+
status: "not_mergeable",
|
|
20957
|
+
reason: `ChangeRequest ${changeRequestId} does not merge cleanly; bring it up to date first`
|
|
20958
|
+
};
|
|
20959
|
+
return { passed: read.head };
|
|
20960
|
+
}
|
|
20961
|
+
}).pipe(catchTags2({
|
|
20962
|
+
WaitTimedOut: (timedOut) => succeed7({
|
|
20963
|
+
status: "not_mergeable",
|
|
20964
|
+
reason: `Nothing happened on ChangeRequest ${changeRequestId} while waiting for ${timedOut.event}`
|
|
20965
|
+
}),
|
|
20966
|
+
ChangeRequestClosed: (closed) => succeed7({
|
|
20967
|
+
status: "not_mergeable",
|
|
20968
|
+
reason: `ChangeRequest ${changeRequestId} was ${closed.merged ? "merged" : "closed"} before Kitman released it`
|
|
20969
|
+
})
|
|
20970
|
+
}));
|
|
20971
|
+
var recovery = (previous) => previous === undefined ? ["Retry the deploy", "Leave it"] : ["Retry the deploy", `Roll back to ${previous}`, "Leave it"];
|
|
20972
|
+
var release_default = defineLoop({
|
|
20973
|
+
id: "release",
|
|
20974
|
+
version: "2.0.0",
|
|
20975
|
+
title: "Release",
|
|
20915
20976
|
input: Input2,
|
|
20916
20977
|
output: Output2,
|
|
20917
|
-
agents: {
|
|
20918
|
-
checkout: "
|
|
20978
|
+
agents: { releaser: "releaser", deployer: "deployer" },
|
|
20979
|
+
checkout: "read",
|
|
20919
20980
|
capabilities: [
|
|
20920
20981
|
"agent_work",
|
|
20921
|
-
"read_change_request",
|
|
20922
|
-
"post_comment",
|
|
20923
|
-
"merge_change_request",
|
|
20924
|
-
"approve_change_request",
|
|
20925
|
-
"call",
|
|
20926
|
-
"write_back",
|
|
20927
20982
|
"ask",
|
|
20928
|
-
"verify"
|
|
20983
|
+
"verify",
|
|
20984
|
+
"claim",
|
|
20985
|
+
"publish_release",
|
|
20986
|
+
"read_change_request",
|
|
20987
|
+
"merge_change_request"
|
|
20929
20988
|
],
|
|
20930
|
-
|
|
20931
|
-
run: (input) => {
|
|
20989
|
+
run: (input) => gen2(function* () {
|
|
20932
20990
|
const changeRequestId = input.changeRequestId;
|
|
20933
|
-
|
|
20934
|
-
|
|
20935
|
-
|
|
20936
|
-
|
|
20937
|
-
|
|
20938
|
-
|
|
20939
|
-
|
|
20940
|
-
|
|
20941
|
-
|
|
20942
|
-
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
const
|
|
20947
|
-
if (
|
|
20948
|
-
|
|
20949
|
-
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
|
|
20991
|
+
let gated;
|
|
20992
|
+
if (changeRequestId === undefined) {
|
|
20993
|
+
const gate = yield* verify("gate", input.revision === undefined ? {} : { revision: input.revision });
|
|
20994
|
+
if (!gate.passed) {
|
|
20995
|
+
const failing = gate.checks.filter((check) => check.status !== "passed").map((check) => check.name);
|
|
20996
|
+
return {
|
|
20997
|
+
status: "gate_failed",
|
|
20998
|
+
reason: `The checks fail at ${gate.revision}: ${failing.join(", ")}`,
|
|
20999
|
+
revision: gate.revision
|
|
21000
|
+
};
|
|
21001
|
+
}
|
|
21002
|
+
gated = gate.revision;
|
|
21003
|
+
} else {
|
|
21004
|
+
const ready = yield* readyToMerge(changeRequestId);
|
|
21005
|
+
if (!("passed" in ready))
|
|
21006
|
+
return ready;
|
|
21007
|
+
gated = ready.passed;
|
|
21008
|
+
}
|
|
21009
|
+
let drafted = yield* agentWork("draft-notes", {
|
|
21010
|
+
agent: "releaser",
|
|
21011
|
+
prompt: draftPrompt(input.tag, changeRequestId),
|
|
21012
|
+
output: Drafted
|
|
20953
21013
|
});
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
|
|
20958
|
-
|
|
20959
|
-
|
|
20960
|
-
|
|
20961
|
-
|
|
20962
|
-
|
|
20963
|
-
|
|
20964
|
-
|
|
20965
|
-
|
|
20966
|
-
|
|
20967
|
-
|
|
20968
|
-
|
|
20969
|
-
|
|
20970
|
-
|
|
20971
|
-
|
|
20972
|
-
|
|
20973
|
-
|
|
20974
|
-
|
|
20975
|
-
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
continue;
|
|
20981
|
-
}
|
|
20982
|
-
if (pipeline.status === "failed" || pipeline.status === "canceled") {
|
|
20983
|
-
if (stalledAt === pipeline.head)
|
|
20984
|
-
stalls++;
|
|
20985
|
-
else
|
|
20986
|
-
stalls = 0;
|
|
20987
|
-
if (stalls >= 2)
|
|
20988
|
-
return done("needs_you", `Two fixes pushed nothing new; the pipeline at ${pipeline.head} still fails`);
|
|
20989
|
-
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
20990
|
-
return done("needs_you", `The pipeline still fails after ${fixRounds} fix rounds`);
|
|
20991
|
-
fixRounds++;
|
|
20992
|
-
stalledAt = pipeline.head;
|
|
20993
|
-
const fixed = yield* agentWork(`ci-fix:${cycle}`, {
|
|
20994
|
-
agent: "implementer",
|
|
20995
|
-
prompt: ciFixPrompt(pipeline, target),
|
|
20996
|
-
output: CiFix,
|
|
20997
|
-
timeout: stepBudget
|
|
20998
|
-
});
|
|
20999
|
-
if (fixed.verdict === "pre_existing")
|
|
21000
|
-
return done("needs_you", `The failure is pre-existing on the target branch: ${fixed.summary}`);
|
|
21001
|
-
if (fixed.verdict === "flaky") {
|
|
21002
|
-
if (pipeline.source === "reported") {
|
|
21003
|
-
if (pipeline.runnerChecks === undefined)
|
|
21004
|
-
return done("needs_you", `The failing job passed when run again, outside the forge: ${fixed.summary}`);
|
|
21005
|
-
if (reranHeads.has(pipeline.head))
|
|
21006
|
-
return done("needs_you", `The failing job failed again when run again on the Runner: ${fixed.summary}`);
|
|
21007
|
-
reranHeads.add(pipeline.head);
|
|
21008
|
-
const failed = yield* runOnRunner("rerun", cycle, pipeline.head, pipeline.runnerChecks);
|
|
21009
|
-
if (failed !== undefined)
|
|
21010
|
-
return done("needs_you", `The pipeline could not run on the Runner: ${failed}`);
|
|
21011
|
-
continue;
|
|
21012
|
-
}
|
|
21013
|
-
if (reranHeads.has(pipeline.head))
|
|
21014
|
-
return done("needs_you", `The failing job failed again when run again on the forge: ${fixed.summary}`);
|
|
21015
|
-
reranHeads.add(pipeline.head);
|
|
21016
|
-
yield* rerunPipeline(`rerun:${cycle}`, { changeRequestId });
|
|
21017
|
-
yield* pipelineMoved(`rerun-wait:${cycle}`, changeRequestId, pipeline).pipe(catchTag2("WaitTimedOut", () => void_3));
|
|
21018
|
-
}
|
|
21019
|
-
continue;
|
|
21020
|
-
}
|
|
21021
|
-
const threads = (yield* reviewThreads(`threads:${cycle}`, { changeRequestId })).threads.filter((thread) => !answered.has(threadKey(thread)));
|
|
21022
|
-
if (threads.length > 0) {
|
|
21023
|
-
const answers = yield* agentWork(`comments:${cycle}`, {
|
|
21024
|
-
agent: "implementer",
|
|
21025
|
-
prompt: repliesPrompt(threads),
|
|
21026
|
-
output: Replies,
|
|
21027
|
-
timeout: stepBudget
|
|
21028
|
-
});
|
|
21029
|
-
for (const answer of answers.replies) {
|
|
21030
|
-
yield* comment(`reply:${cycle}:${answer.threadId}`, {
|
|
21031
|
-
changeRequestId,
|
|
21032
|
-
threadId: answer.threadId,
|
|
21033
|
-
body: answer.reply
|
|
21034
|
-
});
|
|
21035
|
-
replies++;
|
|
21036
|
-
}
|
|
21037
|
-
for (const thread of threads)
|
|
21038
|
-
answered.add(`${thread.id}:${thread.comments.length + 1}`);
|
|
21039
|
-
if (answers.replies.some((answer) => answer.changed))
|
|
21040
|
-
continue;
|
|
21041
|
-
}
|
|
21042
|
-
const commented = {
|
|
21043
|
-
event: "change_request.comment",
|
|
21044
|
-
filter: { changeRequestId },
|
|
21045
|
-
...pipeline.readAt === undefined ? undefined : { since: pipeline.readAt }
|
|
21046
|
-
};
|
|
21047
|
-
const mergeable = yield* waitFor(`mergeable:${cycle}`, {
|
|
21048
|
-
event: "change_request.mergeable",
|
|
21049
|
-
filter: { changeRequestId },
|
|
21050
|
-
timeout: "1 hour",
|
|
21051
|
-
or: [commented]
|
|
21052
|
-
});
|
|
21053
|
-
if (mergeable.type === commented.event)
|
|
21054
|
-
continue;
|
|
21055
|
-
if (mergeable.payload.mergeable === false) {
|
|
21056
|
-
const synced = yield* syncBase(`sync:${cycle}`, { agent: "implementer", target });
|
|
21057
|
-
if (synced.status === "conflict")
|
|
21058
|
-
return done("needs_you", `Merging ${target} in conflicts on ${synced.files.join(", ")}: ${synced.reason}`);
|
|
21059
|
-
if (synced.status === "merged") {
|
|
21060
|
-
const caught = synced.catchUp;
|
|
21061
|
-
const verdict = caught === undefined ? { kind: "changed", why: "its Runner did not report what it did to the branch" } : catchUpVerdict(caught);
|
|
21062
|
-
if (verdict.kind === "changed") {
|
|
21063
|
-
unreviewed = undefined;
|
|
21064
|
-
notCarried = `merging ${target} in changed it: ${verdict.why}`;
|
|
21065
|
-
} else if (caught !== undefined && unreviewed !== undefined) {
|
|
21066
|
-
const files = [...new Set([...unreviewed.files, ...verdict.kind === "review" ? verdict.files : []])];
|
|
21067
|
-
unreviewed = caught.from === unreviewed.synced ? { ...unreviewed, files, synced: caught.head } : { ...unreviewed, files };
|
|
21068
|
-
} else if (caught !== undefined && verdict.kind === "review") {
|
|
21069
|
-
notCarried = undefined;
|
|
21070
|
-
unreviewed = { files: verdict.files, synced: caught.head, sync: `sync:${cycle}`, earlier: [] };
|
|
21071
|
-
} else if (caught !== undefined) {
|
|
21072
|
-
notCarried = undefined;
|
|
21073
|
-
const carried = yield* approveChangeRequest(`carry:${cycle}`, {
|
|
21074
|
-
changeRequestId,
|
|
21075
|
-
head: caught.head,
|
|
21076
|
-
summary: `Merging ${target} in left the branch's own diff as it was approved.`,
|
|
21077
|
-
carries: `sync:${cycle}`
|
|
21078
|
-
}).pipe(catchTag2("StepFailed", (error) => succeed7({ approved: false, reason: error.reason })));
|
|
21079
|
-
if (!carried.approved)
|
|
21080
|
-
notCarried = `the approval was not carried: ${carried.reason}`;
|
|
21081
|
-
}
|
|
21082
|
-
}
|
|
21083
|
-
continue;
|
|
21084
|
-
}
|
|
21085
|
-
let reviewedHere = false;
|
|
21086
|
-
if (unreviewed !== undefined) {
|
|
21087
|
-
const files = unreviewed;
|
|
21088
|
-
const head = pipeline.head;
|
|
21089
|
-
const reviewed = yield* call(`review:${cycle}`, reviewLoop(), {
|
|
21090
|
-
base: `origin/${target}`,
|
|
21091
|
-
head,
|
|
21092
|
-
files: files.files,
|
|
21093
|
-
...head === files.synced ? undefined : { since: files.synced },
|
|
21094
|
-
...files.earlier.length === 0 ? undefined : { earlier: files.earlier }
|
|
21095
|
-
});
|
|
21096
|
-
if (reviewed.blocking) {
|
|
21097
|
-
const blocking = reviewed.findings.filter(isBlocking);
|
|
21098
|
-
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
21099
|
-
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`);
|
|
21100
|
-
fixRounds++;
|
|
21101
|
-
yield* agentWork(`review-fix:${cycle}`, {
|
|
21102
|
-
agent: "implementer",
|
|
21103
|
-
prompt: reviewFixPrompt(blocking),
|
|
21104
|
-
output: ReviewFix,
|
|
21105
|
-
timeout: stepBudget
|
|
21106
|
-
});
|
|
21107
|
-
unreviewed = { ...files, earlier: reviewed.findings };
|
|
21108
|
-
continue;
|
|
21109
|
-
}
|
|
21110
|
-
const approval = yield* approveChangeRequest(`approve:${cycle}`, {
|
|
21111
|
-
changeRequestId,
|
|
21112
|
-
head,
|
|
21113
|
-
summary: `A review of the files merging ${target} in changed (${files.files.join(", ")}) passed with nothing blocking.`,
|
|
21114
|
-
review: `review:${cycle}`,
|
|
21115
|
-
carries: files.sync
|
|
21116
|
-
}).pipe(catchTag2("StepFailed", (error) => succeed7({ approved: false, reason: error.reason })));
|
|
21117
|
-
unreviewed = undefined;
|
|
21118
|
-
if (approval.approved)
|
|
21119
|
-
reviewedHere = true;
|
|
21120
|
-
else
|
|
21121
|
-
notCarried = `merging ${target} in, whose files' review passed but did not approve it: ${approval.reason}`;
|
|
21122
|
-
}
|
|
21123
|
-
const moved = pipeline.readAt === undefined ? [] : [
|
|
21124
|
-
{ event: "change_request.pipeline", filter: { changeRequestId }, since: pipeline.readAt },
|
|
21125
|
-
...pipeline.source === "reported" ? [
|
|
21126
|
-
{
|
|
21127
|
-
event: "change_request.approval",
|
|
21128
|
-
filter: { changeRequestId, approved: true },
|
|
21129
|
-
since: pipeline.readAt
|
|
21130
|
-
}
|
|
21131
|
-
] : []
|
|
21132
|
-
];
|
|
21133
|
-
const approved = reviewedHere ? undefined : yield* waitFor(`approved:${cycle}`, {
|
|
21134
|
-
event: "change_request.approval",
|
|
21135
|
-
filter: { changeRequestId, approved: true, head: pipeline.head },
|
|
21136
|
-
timeout: "7 days",
|
|
21137
|
-
or: [commented, ...moved]
|
|
21014
|
+
const tag = input.tag ?? drafted.tag;
|
|
21015
|
+
for (let round = 1;; round++) {
|
|
21016
|
+
const answer = yield* ask(round === 1 ? "approve" : `approve:${round}`, {
|
|
21017
|
+
prompt: `Release ${tag} with these notes?
|
|
21018
|
+
|
|
21019
|
+
${drafted.notes}`,
|
|
21020
|
+
options: [
|
|
21021
|
+
{ value: "Approve", description: "Tags, publishes and deploys it." },
|
|
21022
|
+
{ value: "Edit notes", description: "Say what to change; the notes are redrafted." },
|
|
21023
|
+
{ value: "Reject", description: "Keeps the draft and releases nothing." }
|
|
21024
|
+
],
|
|
21025
|
+
recommended: "Approve",
|
|
21026
|
+
recommendationReason: "The release is proved: its checks pass, or its ChangeRequest is green, approved and mergeable.",
|
|
21027
|
+
default: null,
|
|
21028
|
+
auto: "Approve"
|
|
21029
|
+
});
|
|
21030
|
+
if (answer === "Approve")
|
|
21031
|
+
break;
|
|
21032
|
+
if (answer === "Reject") {
|
|
21033
|
+
const why = yield* ask("reject-why", {
|
|
21034
|
+
prompt: `Why not release ${tag}? The draft is kept.`,
|
|
21035
|
+
freeText: true,
|
|
21036
|
+
informationOnly: true,
|
|
21037
|
+
recommendationReason: "Only you know why you rejected it.",
|
|
21038
|
+
default: "No reason given",
|
|
21039
|
+
timeout: "1 day"
|
|
21138
21040
|
});
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
prompt: "The ChangeRequest is approved, green and mergeable. Merge now?",
|
|
21145
|
-
options: [
|
|
21146
|
-
{ value: "Merge", description: "Merges the head whose pipeline passed." },
|
|
21147
|
-
{ value: "Wait", description: "Leaves the merge to a person." }
|
|
21148
|
-
],
|
|
21149
|
-
recommended: "Merge",
|
|
21150
|
-
recommendationReason: "It is approved, green and mergeable, which is everything a merge waits for.",
|
|
21151
|
-
default: "Wait",
|
|
21152
|
-
timeout: "1 hour"
|
|
21041
|
+
const kept = yield* artifact("notes", {
|
|
21042
|
+
kind: "release_notes",
|
|
21043
|
+
title: drafted.title,
|
|
21044
|
+
text: drafted.notes,
|
|
21045
|
+
data: { tag, changeRequests: drafted.changeRequests }
|
|
21153
21046
|
});
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21047
|
+
return { status: "rejected", reason: why, tag, notesArtifactId: kept.artifactId };
|
|
21048
|
+
}
|
|
21049
|
+
const edit = yield* ask(`edit:${round}`, {
|
|
21050
|
+
prompt: "What should change in the notes?",
|
|
21051
|
+
freeText: true,
|
|
21052
|
+
informationOnly: true,
|
|
21053
|
+
recommendationReason: "You chose to edit them, so only you know what should change.",
|
|
21054
|
+
default: "",
|
|
21055
|
+
timeout: "1 day"
|
|
21056
|
+
});
|
|
21057
|
+
if (edit.trim() === "")
|
|
21058
|
+
continue;
|
|
21059
|
+
drafted = yield* agentWork(`redraft:${round}`, {
|
|
21060
|
+
agent: "releaser",
|
|
21061
|
+
prompt: redraftPrompt(drafted, edit),
|
|
21062
|
+
output: Drafted
|
|
21063
|
+
});
|
|
21064
|
+
}
|
|
21065
|
+
const notes = yield* artifact("notes", {
|
|
21066
|
+
kind: "release_notes",
|
|
21067
|
+
title: drafted.title,
|
|
21068
|
+
text: drafted.notes,
|
|
21069
|
+
data: { tag, changeRequests: drafted.changeRequests }
|
|
21070
|
+
});
|
|
21071
|
+
let revision = gated;
|
|
21072
|
+
if (changeRequestId !== undefined) {
|
|
21073
|
+
for (let round = 1;; round++) {
|
|
21074
|
+
const merged = yield* mergeChangeRequest(round === 1 ? "merge" : `merge:${round}`, {
|
|
21157
21075
|
changeRequestId,
|
|
21158
|
-
head:
|
|
21076
|
+
head: gated
|
|
21159
21077
|
});
|
|
21160
|
-
if (merged.status
|
|
21161
|
-
|
|
21162
|
-
|
|
21163
|
-
|
|
21164
|
-
|
|
21165
|
-
|
|
21166
|
-
|
|
21167
|
-
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
}
|
|
21171
|
-
|
|
21172
|
-
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21176
|
-
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
|
|
21182
|
-
|
|
21183
|
-
|
|
21184
|
-
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
|
|
21078
|
+
if (merged.status === "conflict")
|
|
21079
|
+
return {
|
|
21080
|
+
status: "not_mergeable",
|
|
21081
|
+
reason: `ChangeRequest ${changeRequestId} no longer merges cleanly`,
|
|
21082
|
+
tag,
|
|
21083
|
+
notesArtifactId: notes.artifactId
|
|
21084
|
+
};
|
|
21085
|
+
if (merged.status === "merged") {
|
|
21086
|
+
revision = merged.revision;
|
|
21087
|
+
break;
|
|
21088
|
+
}
|
|
21089
|
+
if (round >= 3)
|
|
21090
|
+
return {
|
|
21091
|
+
status: "not_mergeable",
|
|
21092
|
+
reason: `ChangeRequest ${changeRequestId} kept moving while it was gated; it is at ${merged.head}`,
|
|
21093
|
+
tag,
|
|
21094
|
+
notesArtifactId: notes.artifactId
|
|
21095
|
+
};
|
|
21096
|
+
const ready = yield* readyToMerge(changeRequestId, round + 1);
|
|
21097
|
+
if (!("passed" in ready))
|
|
21098
|
+
return { ...ready, tag, notesArtifactId: notes.artifactId };
|
|
21099
|
+
gated = ready.passed;
|
|
21100
|
+
}
|
|
21101
|
+
}
|
|
21102
|
+
const deployEvent = input.deployEvent ?? "deploy";
|
|
21103
|
+
yield* claim("deploy-target", { resource: `deploy:${input.deployTarget ?? deployEvent}` });
|
|
21104
|
+
const released = yield* release2("tag", {
|
|
21105
|
+
tag,
|
|
21106
|
+
revision,
|
|
21107
|
+
title: drafted.title,
|
|
21108
|
+
notes: drafted.notes
|
|
21109
|
+
});
|
|
21110
|
+
const deployed = (step, deployTag) => waitFor(step, {
|
|
21111
|
+
event: deployEvent,
|
|
21112
|
+
filter: { tag: deployTag },
|
|
21113
|
+
timeout: `${input.deployTimeoutMinutes ?? 60} minutes`
|
|
21114
|
+
}).pipe(map8((deploy) => deploy.payload.status === "failed" ? "deploy_failed" : "deployed"), catchTag2("WaitTimedOut", () => succeed7("timed_out")));
|
|
21115
|
+
const settled = { tag, notesArtifactId: notes.artifactId, revision: released.revision, url: released.url };
|
|
21116
|
+
const ran = yield* agentWork("run-deploy", {
|
|
21117
|
+
agent: "deployer",
|
|
21118
|
+
prompt: deployPrompt(tag, drafted.previousTag),
|
|
21119
|
+
output: Deployed,
|
|
21120
|
+
timeout: "3 hours"
|
|
21121
|
+
});
|
|
21122
|
+
if (ran.status !== "none") {
|
|
21123
|
+
const status = ran.status === "failed" ? "deploy_failed" : ran.status;
|
|
21124
|
+
const outcome = {
|
|
21125
|
+
deployed: "deployed",
|
|
21126
|
+
rolled_back: "rolled back",
|
|
21127
|
+
deploy_failed: "failed to deploy",
|
|
21128
|
+
rollback_failed: "failed to deploy, and the rollback failed too"
|
|
21129
|
+
}[status];
|
|
21130
|
+
yield* artifact("report", {
|
|
21131
|
+
kind: "release_report",
|
|
21132
|
+
title: `${tag} ${outcome}`,
|
|
21133
|
+
text: `${tag} at ${released.revision}: ${ran.detail}. ${released.url}`,
|
|
21134
|
+
data: { tag, status, revision: released.revision, url: released.url, detail: ran.detail }
|
|
21135
|
+
});
|
|
21136
|
+
return { status, reason: ran.detail, ...settled };
|
|
21137
|
+
}
|
|
21138
|
+
let status = yield* deployed("deploy", tag);
|
|
21139
|
+
for (let round = 1;status !== "deployed"; round++) {
|
|
21140
|
+
const options = recovery(drafted.previousTag);
|
|
21141
|
+
const answer = yield* ask(`recover:${round}`, {
|
|
21142
|
+
prompt: `${tag} ${status === "timed_out" ? "did not report a deploy in time" : "failed to deploy"}. What now?`,
|
|
21143
|
+
options,
|
|
21144
|
+
recommended: "Retry the deploy",
|
|
21145
|
+
recommendationReason: "A failed or late deploy is often passing, and a retry runs only the failed jobs of the same tag.",
|
|
21146
|
+
default: "Leave it",
|
|
21147
|
+
timeout: "1 hour"
|
|
21148
|
+
});
|
|
21149
|
+
if (answer === "Leave it")
|
|
21150
|
+
return { status, reason: `${tag} at ${released.revision} was left undeployed`, ...settled };
|
|
21151
|
+
if (answer === "Retry the deploy") {
|
|
21152
|
+
yield* rerunPipeline(`deploy:rerun-${round}`, { ref: tag });
|
|
21153
|
+
status = yield* deployed(`deploy:retry-${round}`, tag);
|
|
21154
|
+
continue;
|
|
21155
|
+
}
|
|
21156
|
+
const previous = drafted.previousTag ?? tag;
|
|
21157
|
+
yield* rerunPipeline(`deploy:redeploy-${round}`, { ref: previous, all: true });
|
|
21158
|
+
const back = yield* deployed(`deploy:rollback-${round}`, previous);
|
|
21159
|
+
if (back === "deployed")
|
|
21160
|
+
return { status: "rolled_back", reason: `Rolled back to ${previous}`, ...settled };
|
|
21161
|
+
status = back;
|
|
21162
|
+
}
|
|
21163
|
+
yield* artifact("report", {
|
|
21164
|
+
kind: "release_report",
|
|
21165
|
+
title: `${tag} deployed`,
|
|
21166
|
+
text: `${tag} at ${released.revision} deployed. ${released.url}`,
|
|
21167
|
+
data: { tag, status, revision: released.revision, url: released.url }
|
|
21168
|
+
});
|
|
21169
|
+
return { status: "deployed", reason: `${tag} deployed`, ...settled };
|
|
21170
|
+
})
|
|
21189
21171
|
});
|
|
21190
|
-
|
|
21191
|
-
|
|
21192
|
-
|
|
21193
|
-
|
|
21194
|
-
|
|
21172
|
+
|
|
21173
|
+
// src/shepherd-output.ts
|
|
21174
|
+
var ShepherdOutput = Struct({
|
|
21175
|
+
status: Literals(["merged", "ready", "needs_you"]),
|
|
21176
|
+
reason: String5,
|
|
21177
|
+
fixRounds: Int,
|
|
21178
|
+
replies: Int,
|
|
21179
|
+
release: optional2(Struct({ runStatus: String5, reason: String5, tag: optional2(String5) }))
|
|
21195
21180
|
});
|
|
21196
|
-
|
|
21197
|
-
|
|
21198
|
-
|
|
21199
|
-
|
|
21200
|
-
|
|
21201
|
-
|
|
21202
|
-
|
|
21203
|
-
|
|
21181
|
+
|
|
21182
|
+
// src/shepherd.ts
|
|
21183
|
+
var reviewLoop = () => ({ id: "review", input: Input, output: Output });
|
|
21184
|
+
var stepBudget = "45 minutes";
|
|
21185
|
+
var Input3 = Struct({
|
|
21186
|
+
changeRequestId: String5,
|
|
21187
|
+
taskId: optional2(String5),
|
|
21188
|
+
merge: optional2(Boolean3),
|
|
21189
|
+
branch: optional2(String5),
|
|
21190
|
+
continuing: optional2(Continuing),
|
|
21191
|
+
maxFixRounds: optional2(Int),
|
|
21192
|
+
maxCycles: optional2(Int)
|
|
21204
21193
|
});
|
|
21205
|
-
var
|
|
21206
|
-
|
|
21194
|
+
var Output3 = ShepherdOutput;
|
|
21195
|
+
var CiFix = Struct({
|
|
21196
|
+
verdict: Literals(["fixed", "pre_existing", "flaky"]),
|
|
21207
21197
|
summary: String5
|
|
21208
21198
|
});
|
|
21209
|
-
var
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
claims: ArraySchema(String5),
|
|
21216
|
-
rebuttals: ArraySchema(Rebuttal)
|
|
21217
|
-
});
|
|
21218
|
-
var BuildOutput = Struct({
|
|
21219
|
-
changeRequest: ChangeRequestResult,
|
|
21220
|
-
revision: String5,
|
|
21221
|
-
tree: String5,
|
|
21222
|
-
settled: Boolean3,
|
|
21223
|
-
unsettled: ArraySchema(String5),
|
|
21224
|
-
rounds: Int,
|
|
21225
|
-
stopped: Literals(["settled", "dispute_stands", "findings_returned", "round_cap"]),
|
|
21226
|
-
criteria: ArraySchema(CriterionAnswer),
|
|
21227
|
-
approval: optional2(ApproveChangeRequestResult),
|
|
21228
|
-
landed: optional2(ShepherdOutput),
|
|
21229
|
-
next: Struct({ changeRequestId: String5 })
|
|
21199
|
+
var Replies = Struct({
|
|
21200
|
+
replies: ArraySchema(Struct({
|
|
21201
|
+
threadId: String5,
|
|
21202
|
+
reply: String5,
|
|
21203
|
+
changed: Boolean3
|
|
21204
|
+
}))
|
|
21230
21205
|
});
|
|
21206
|
+
var failedJobs = (pipeline) => pipeline.jobs.flatMap((job) => job.status === "failed" ? [`### ${job.name}${job.url === undefined ? "" : ` (${job.url})`}
|
|
21231
21207
|
|
|
21232
|
-
|
|
21233
|
-
|
|
21234
|
-
}
|
|
21235
|
-
|
|
21236
|
-
|
|
21237
|
-
|
|
21238
|
-
|
|
21239
|
-
refactor: "Keep behaviour exactly as it is; the approved verifications must pass before and after.",
|
|
21240
|
-
investigation: "Investigate and report what you found, with its sources.",
|
|
21241
|
-
docs: "Name, in `documented`, the approved checks that run the commands you documented.",
|
|
21242
|
-
migration: "Name, in `rollback`, the approved check that rolls the migration back.",
|
|
21243
|
-
performance: "Make it faster without changing behaviour."
|
|
21244
|
-
};
|
|
21245
|
-
|
|
21246
|
-
class BuildOutcomeRefused extends TaggedError3()("BuildOutcomeRefused", {
|
|
21247
|
-
message: String5
|
|
21248
|
-
}) {
|
|
21249
|
-
}
|
|
21250
|
-
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.";
|
|
21251
|
-
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.";
|
|
21252
|
-
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.";
|
|
21253
|
-
var implementPrompt = (task, outcome, continuing) => [
|
|
21254
|
-
...continuing === undefined ? [] : [continuingBrief(continuing), "Fix forward from there."],
|
|
21255
|
-
`Build ${task}`,
|
|
21256
|
-
implementAdaptation,
|
|
21257
|
-
...outcome === undefined ? [] : [outcomeGuidance[outcome]],
|
|
21258
|
-
'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".',
|
|
21259
|
-
criteriaGuidance,
|
|
21260
|
-
checksGuidance
|
|
21208
|
+
${job.logTail ?? "No log was served."}`] : []);
|
|
21209
|
+
var ciFixPrompt = (pipeline, target) => [
|
|
21210
|
+
`The pipeline at ${pipeline.head} failed. These jobs failed, with the end of each log:`,
|
|
21211
|
+
...failedJobs(pipeline),
|
|
21212
|
+
"Run the failing job once more here to rule out a flake; if it passes, change nothing and answer flaky.",
|
|
21213
|
+
`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.`,
|
|
21214
|
+
"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."
|
|
21261
21215
|
].join(`
|
|
21262
21216
|
|
|
21263
21217
|
`);
|
|
21264
|
-
var
|
|
21265
|
-
|
|
21266
|
-
...
|
|
21267
|
-
criteriaGuidance
|
|
21218
|
+
var threadLine = (thread) => [
|
|
21219
|
+
`### Thread ${thread.id}${thread.path === undefined ? "" : ` on ${thread.path}${thread.line === undefined ? "" : `:${thread.line}`}`}`,
|
|
21220
|
+
...thread.comments.map((entry) => `${entry.author ?? "someone"}: ${entry.body}`)
|
|
21268
21221
|
].join(`
|
|
21269
|
-
|
|
21270
21222
|
`);
|
|
21271
|
-
var
|
|
21272
|
-
var
|
|
21273
|
-
|
|
21274
|
-
|
|
21223
|
+
var ReviewFix = Struct({ summary: String5 });
|
|
21224
|
+
var reviewFixPrompt = (findings) => [
|
|
21225
|
+
"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.",
|
|
21226
|
+
...findings.map((finding) => `- ${finding.id} (${finding.severity}): ${finding.detail}`)
|
|
21275
21227
|
].join(`
|
|
21276
|
-
|
|
21277
21228
|
`);
|
|
21278
|
-
var
|
|
21279
|
-
|
|
21280
|
-
|
|
21281
|
-
|
|
21229
|
+
var catchUpVerdict = (catchUp) => {
|
|
21230
|
+
if (catchUp.truncated)
|
|
21231
|
+
return { kind: "changed", why: "it changed more files than the Runner compares" };
|
|
21232
|
+
const files = [...new Set([...catchUp.conflicted, ...catchUp.rewritten])];
|
|
21233
|
+
const elsewhere = catchUp.changed.filter((file) => !files.includes(file));
|
|
21234
|
+
if (elsewhere.length > 0)
|
|
21235
|
+
return { kind: "changed", why: `the branch's own diff changed in ${elsewhere.join(", ")}` };
|
|
21236
|
+
return files.length === 0 ? { kind: "carry" } : { kind: "review", files };
|
|
21237
|
+
};
|
|
21238
|
+
var repliesPrompt = (threads) => [
|
|
21239
|
+
"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.",
|
|
21240
|
+
"Never mark a thread resolved: whether a disputed point is settled is the reviewer's call.",
|
|
21241
|
+
...threads.map(threadLine)
|
|
21282
21242
|
].join(`
|
|
21283
21243
|
|
|
21284
21244
|
`);
|
|
21285
|
-
var
|
|
21286
|
-
var
|
|
21287
|
-
|
|
21288
|
-
|
|
21289
|
-
|
|
21290
|
-
|
|
21291
|
-
|
|
21292
|
-
...checks.length === 0 ? [] : [
|
|
21293
|
-
"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.",
|
|
21294
|
-
...checks.map(findingLine)
|
|
21295
|
-
],
|
|
21296
|
-
...findings.length === 0 ? [] : [
|
|
21297
|
-
"The review found these. Fix each, or rebut it with your reason if it is wrong; reviewers judge the rebuttals next round.",
|
|
21298
|
-
...findings.map(findingLine)
|
|
21299
|
-
],
|
|
21300
|
-
...settled.standing.length === 0 ? [] : [
|
|
21301
|
-
"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.",
|
|
21302
|
-
...settled.standing.map(disputeLine)
|
|
21303
|
-
]
|
|
21304
|
-
].join(`
|
|
21305
|
-
`);
|
|
21306
|
-
};
|
|
21307
|
-
var blockingIds = (settled) => settled.open.filter(isBlocking).map((finding) => finding.id);
|
|
21308
|
-
var onlyChecksBlock = (settled) => blockingIds(settled).every(isCheckFinding) && !settled.standing.some((entry) => isBlocking(entry.finding));
|
|
21309
|
-
var rally = (input) => gen2(function* () {
|
|
21310
|
-
const claims = [...input.claims];
|
|
21311
|
-
const conflicts = [];
|
|
21312
|
-
let rebuttals = [];
|
|
21313
|
-
let earlier = [];
|
|
21314
|
-
let previous;
|
|
21315
|
-
let last;
|
|
21316
|
-
const shown = new Set;
|
|
21317
|
-
for (let round = 1;; round++) {
|
|
21318
|
-
const synced = yield* syncBase(`sync:${round}`, {
|
|
21319
|
-
agent: "implementer",
|
|
21320
|
-
...input.target === undefined ? undefined : { target: input.target }
|
|
21321
|
-
});
|
|
21322
|
-
if (synced.status === "conflict")
|
|
21323
|
-
conflicts.push(`Could not merge the target branch in round ${round}: ${synced.files.join(", ")} (${synced.reason})`);
|
|
21324
|
-
const head = yield* verify(`verify:${round}`, { suggested: input.suggested });
|
|
21325
|
-
if (last !== undefined && last.head.tree === head.tree && rebuttals.length === 0 && onlyChecksBlock(last.settled))
|
|
21326
|
-
return {
|
|
21327
|
-
...last,
|
|
21328
|
-
round: round - 1,
|
|
21329
|
-
head,
|
|
21330
|
-
decision: { stop: true, reason: "findings_returned", unsettled: blockingIds(last.settled) },
|
|
21331
|
-
claims,
|
|
21332
|
-
conflicts
|
|
21333
|
-
};
|
|
21334
|
-
const failing = brokenChecks(head).map((check) => check.name);
|
|
21335
|
-
const reviewed = yield* call(`review:${round}`, review_default, {
|
|
21336
|
-
base: input.base,
|
|
21337
|
-
head: head.revision,
|
|
21338
|
-
...rebuttals.length === 0 ? undefined : { rebuttals },
|
|
21339
|
-
...earlier.length === 0 ? undefined : { earlier },
|
|
21340
|
-
...input.outcome === undefined ? undefined : { outcome: input.outcome },
|
|
21341
|
-
...failing.length === 0 ? undefined : { failing },
|
|
21342
|
-
claims: [...claims]
|
|
21343
|
-
});
|
|
21344
|
-
const checks = checkFindings(head);
|
|
21345
|
-
const settled = settleRound({
|
|
21346
|
-
findings: [...reviewed.findings, ...scopeFindings(reviewed.scope), ...checks],
|
|
21347
|
-
rebuttals,
|
|
21348
|
-
verdicts: reviewed.verdicts
|
|
21349
|
-
});
|
|
21350
|
-
earlier = [...settled.open, ...settled.standing.map((entry) => entry.finding)].filter((finding) => !finding.id.startsWith("scope: ") && !isCheckFinding(finding.id));
|
|
21351
|
-
const unseen = checks.some((finding) => !shown.has(finding.detail));
|
|
21352
|
-
const decision = rallyStop({
|
|
21353
|
-
round,
|
|
21354
|
-
maxRounds: input.maxRounds,
|
|
21355
|
-
settled,
|
|
21356
|
-
...previous === undefined || unseen ? undefined : { previous }
|
|
21357
|
-
});
|
|
21358
|
-
if (decision.stop && (!unseen || round >= input.maxRounds))
|
|
21359
|
-
return { round, head, settled, decision, claims, scope: reviewed.scope, conflicts };
|
|
21360
|
-
for (const finding of checks)
|
|
21361
|
-
shown.add(finding.detail);
|
|
21362
|
-
const fixed = yield* agentWork(`fix:${round}`, {
|
|
21363
|
-
agent: "implementer",
|
|
21364
|
-
prompt: fixPrompt(settled),
|
|
21365
|
-
output: Fixed
|
|
21366
|
-
});
|
|
21367
|
-
claims.push(...fixed.claims, ...fixed.rebuttals.flatMap((rebuttal) => isCheckFinding(rebuttal.findingId) ? [`Rebutted ${rebuttal.findingId}: ${rebuttal.reason}`] : []));
|
|
21368
|
-
const rebutted = fixed.rebuttals.filter((rebuttal) => !isCheckFinding(rebuttal.findingId));
|
|
21369
|
-
rebuttals = [
|
|
21370
|
-
...rebutted,
|
|
21371
|
-
...settled.standing.filter((entry) => isBlocking(entry.finding)).map((entry) => entry.rebuttal).filter((rebuttal) => !rebutted.some((entry) => entry.findingId === rebuttal.findingId))
|
|
21372
|
-
];
|
|
21373
|
-
previous = blockingIds(settled);
|
|
21374
|
-
last = { head, settled, scope: reviewed.scope };
|
|
21375
|
-
}
|
|
21376
|
-
});
|
|
21377
|
-
var build_default = defineLoop({
|
|
21378
|
-
id: "build",
|
|
21379
|
-
version: "2.2.0",
|
|
21380
|
-
title: "Build",
|
|
21381
|
-
input: BuildInput,
|
|
21382
|
-
output: BuildOutput,
|
|
21245
|
+
var threadKey = (thread) => `${thread.id}:${thread.comments.length}`;
|
|
21246
|
+
var shepherd_default = defineLoop({
|
|
21247
|
+
id: "shepherd",
|
|
21248
|
+
version: "1.1.0",
|
|
21249
|
+
title: "Shepherd",
|
|
21250
|
+
input: Input3,
|
|
21251
|
+
output: Output3,
|
|
21383
21252
|
agents: { implementer: "implementer" },
|
|
21384
21253
|
checkout: "write",
|
|
21385
|
-
|
|
21386
|
-
|
|
21387
|
-
|
|
21388
|
-
|
|
21389
|
-
|
|
21254
|
+
capabilities: [
|
|
21255
|
+
"agent_work",
|
|
21256
|
+
"read_change_request",
|
|
21257
|
+
"post_comment",
|
|
21258
|
+
"merge_change_request",
|
|
21259
|
+
"approve_change_request",
|
|
21260
|
+
"call",
|
|
21261
|
+
"write_back",
|
|
21262
|
+
"ask",
|
|
21263
|
+
"verify"
|
|
21390
21264
|
],
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
|
|
21401
|
-
|
|
21402
|
-
|
|
21403
|
-
|
|
21404
|
-
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
});
|
|
21408
|
-
|
|
21409
|
-
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
21414
|
-
const implemented = yield* agentWorkOnce("implement", {
|
|
21415
|
-
agent: "implementer",
|
|
21416
|
-
skill: "implement",
|
|
21417
|
-
prompt: implementPrompt(task, outcome, input.continuing),
|
|
21418
|
-
output: Implemented
|
|
21419
|
-
});
|
|
21420
|
-
const unanswered = input.taskId !== undefined && implemented.criteria === undefined;
|
|
21421
|
-
const unmet = (implemented.criteria ?? []).filter((answer) => answer.status === "not_met");
|
|
21422
|
-
const criteria = unanswered || unmet.length > 0 ? (yield* agentWorkOnce("criteria", {
|
|
21423
|
-
agent: "implementer",
|
|
21424
|
-
prompt: criteriaPrompt(unanswered, unmet),
|
|
21425
|
-
output: Criteria
|
|
21426
|
-
})).criteria : implemented.criteria ?? [];
|
|
21427
|
-
const last = yield* rally({
|
|
21428
|
-
maxRounds: input.maxRounds ?? 3,
|
|
21429
|
-
base: `origin/${target ?? "HEAD"}`,
|
|
21430
|
-
target,
|
|
21431
|
-
outcome,
|
|
21432
|
-
claims: [...implemented.claims, ...criteria.map((answer) => `Criterion ${answerLine(answer)}`)],
|
|
21433
|
-
suggested: implemented.checks ?? []
|
|
21434
|
-
});
|
|
21435
|
-
const verdict = evidenceGate({
|
|
21436
|
-
...outcome === undefined ? undefined : { outcome },
|
|
21437
|
-
head: last.head,
|
|
21438
|
-
...base === undefined ? undefined : { base },
|
|
21439
|
-
...last.scope === undefined ? undefined : { review: last.scope },
|
|
21440
|
-
...reproduced === undefined ? undefined : { reproducing: reproduced.check },
|
|
21441
|
-
...implemented.documented === undefined ? undefined : { documented: implemented.documented },
|
|
21442
|
-
...implemented.rollback === undefined ? undefined : { rollback: implemented.rollback }
|
|
21443
|
-
});
|
|
21444
|
-
const unsettled = [
|
|
21445
|
-
...new Set([
|
|
21446
|
-
...verdict.settled ? [] : verdict.reasons,
|
|
21447
|
-
...brokenChecks(last.head).map((check) => failsOn("head", check.name, last.head.tree)),
|
|
21448
|
-
...unsettledItems(last.settled, last.decision),
|
|
21449
|
-
...last.conflicts,
|
|
21450
|
-
...criteria.flatMap((answer) => answer.status === "not_met" ? [`Criterion not met: ${answer.criterion} (${answer.detail})`] : [])
|
|
21451
|
-
])
|
|
21452
|
-
];
|
|
21453
|
-
const body = changeRequestBody({
|
|
21454
|
-
summary: implemented.summary,
|
|
21455
|
-
head: last.head,
|
|
21456
|
-
claims: last.claims,
|
|
21457
|
-
unsettled,
|
|
21458
|
-
criteria: criteria.map(answerLine),
|
|
21459
|
-
later: last.settled.open.filter((finding) => !isBlocking(finding)).map((finding) => `${finding.id}: ${finding.detail}`),
|
|
21460
|
-
rounds: last.round,
|
|
21461
|
-
stopped: last.decision.reason
|
|
21265
|
+
calls: () => [review_default, release_default],
|
|
21266
|
+
run: (input) => {
|
|
21267
|
+
const changeRequestId = input.changeRequestId;
|
|
21268
|
+
const target = input.continuing?.targetBranch ?? input.branch ?? "HEAD";
|
|
21269
|
+
const answered = new Set;
|
|
21270
|
+
const reranHeads = new Set;
|
|
21271
|
+
const ranOnRunner = new Set;
|
|
21272
|
+
let fixRounds = 0;
|
|
21273
|
+
let replies = 0;
|
|
21274
|
+
let stalledAt;
|
|
21275
|
+
let stalls = 0;
|
|
21276
|
+
let unreviewed;
|
|
21277
|
+
let notCarried;
|
|
21278
|
+
const done = (status, reason) => ({ status, reason, fixRounds, replies });
|
|
21279
|
+
const runOnRunner = (step, cycle, head, checks) => gen2(function* () {
|
|
21280
|
+
const verification = `pipeline-${step}:${cycle}`;
|
|
21281
|
+
const failed = yield* verify(verification, { revision: head, checks }).pipe(as2(undefined), catchTag2("StepFailed", (error) => succeed7(error.reason)));
|
|
21282
|
+
if (failed === undefined)
|
|
21283
|
+
yield* reportPipeline(`pipeline-${step === "run" ? "report" : "rereport"}:${cycle}`, {
|
|
21284
|
+
changeRequestId,
|
|
21285
|
+
verification
|
|
21286
|
+
});
|
|
21287
|
+
return failed;
|
|
21462
21288
|
});
|
|
21463
|
-
|
|
21464
|
-
|
|
21465
|
-
|
|
21466
|
-
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
|
|
21475
|
-
|
|
21476
|
-
|
|
21477
|
-
|
|
21478
|
-
|
|
21479
|
-
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
|
|
21490
|
-
|
|
21491
|
-
|
|
21289
|
+
return gen2(function* () {
|
|
21290
|
+
for (let cycle = 1;; cycle++) {
|
|
21291
|
+
if (cycle > (input.maxCycles ?? 20)) {
|
|
21292
|
+
const goOn = yield* ask(`continue:${cycle}`, {
|
|
21293
|
+
prompt: `Shepherd has looked at this ChangeRequest ${cycle - 1} times without landing it. Keep going?`,
|
|
21294
|
+
options: ["Keep going", "Stop"],
|
|
21295
|
+
recommended: "Stop",
|
|
21296
|
+
recommendationReason: `${cycle - 1} looks have not landed it, so something needs a person rather than another look.`,
|
|
21297
|
+
default: "Stop",
|
|
21298
|
+
timeout: "1 hour"
|
|
21299
|
+
});
|
|
21300
|
+
if (goOn === "Stop")
|
|
21301
|
+
return done("needs_you", `Stopped after ${cycle - 1} looks without landing it`);
|
|
21302
|
+
}
|
|
21303
|
+
const pipeline = yield* pipelineStatus(`pipeline:${cycle}`, { changeRequestId });
|
|
21304
|
+
if (pipeline.status === "none" && pipeline.runnerChecks !== undefined) {
|
|
21305
|
+
if (ranOnRunner.has(pipeline.head))
|
|
21306
|
+
return done("needs_you", `The pipeline ran on the Runner at ${pipeline.head}, but no result was recorded`);
|
|
21307
|
+
ranOnRunner.add(pipeline.head);
|
|
21308
|
+
const failed = yield* runOnRunner("run", cycle, pipeline.head, pipeline.runnerChecks);
|
|
21309
|
+
if (failed !== undefined)
|
|
21310
|
+
return done("needs_you", `The pipeline could not run on the Runner: ${failed}`);
|
|
21311
|
+
continue;
|
|
21312
|
+
}
|
|
21313
|
+
if (pipeline.status === "none" || pipeline.status === "running") {
|
|
21314
|
+
yield* pipelineMoved(`wait:${cycle}`, changeRequestId, pipeline);
|
|
21315
|
+
continue;
|
|
21316
|
+
}
|
|
21317
|
+
if (pipeline.status === "failed" || pipeline.status === "canceled") {
|
|
21318
|
+
if (stalledAt === pipeline.head)
|
|
21319
|
+
stalls++;
|
|
21320
|
+
else
|
|
21321
|
+
stalls = 0;
|
|
21322
|
+
if (stalls >= 2)
|
|
21323
|
+
return done("needs_you", `Two fixes pushed nothing new; the pipeline at ${pipeline.head} still fails`);
|
|
21324
|
+
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
21325
|
+
return done("needs_you", `The pipeline still fails after ${fixRounds} fix rounds`);
|
|
21326
|
+
fixRounds++;
|
|
21327
|
+
stalledAt = pipeline.head;
|
|
21328
|
+
const fixed = yield* agentWork(`ci-fix:${cycle}`, {
|
|
21329
|
+
agent: "implementer",
|
|
21330
|
+
prompt: ciFixPrompt(pipeline, target),
|
|
21331
|
+
output: CiFix,
|
|
21332
|
+
timeout: stepBudget
|
|
21333
|
+
});
|
|
21334
|
+
if (fixed.verdict === "pre_existing")
|
|
21335
|
+
return done("needs_you", `The failure is pre-existing on the target branch: ${fixed.summary}`);
|
|
21336
|
+
if (fixed.verdict === "flaky") {
|
|
21337
|
+
if (pipeline.source === "reported") {
|
|
21338
|
+
if (pipeline.runnerChecks === undefined)
|
|
21339
|
+
return done("needs_you", `The failing job passed when run again, outside the forge: ${fixed.summary}`);
|
|
21340
|
+
if (reranHeads.has(pipeline.head))
|
|
21341
|
+
return done("needs_you", `The failing job failed again when run again on the Runner: ${fixed.summary}`);
|
|
21342
|
+
reranHeads.add(pipeline.head);
|
|
21343
|
+
const failed = yield* runOnRunner("rerun", cycle, pipeline.head, pipeline.runnerChecks);
|
|
21344
|
+
if (failed !== undefined)
|
|
21345
|
+
return done("needs_you", `The pipeline could not run on the Runner: ${failed}`);
|
|
21346
|
+
continue;
|
|
21347
|
+
}
|
|
21348
|
+
if (reranHeads.has(pipeline.head))
|
|
21349
|
+
return done("needs_you", `The failing job failed again when run again on the forge: ${fixed.summary}`);
|
|
21350
|
+
reranHeads.add(pipeline.head);
|
|
21351
|
+
yield* rerunPipeline(`rerun:${cycle}`, { changeRequestId });
|
|
21352
|
+
yield* pipelineMoved(`rerun-wait:${cycle}`, changeRequestId, pipeline).pipe(catchTag2("WaitTimedOut", () => void_3));
|
|
21353
|
+
}
|
|
21354
|
+
continue;
|
|
21355
|
+
}
|
|
21356
|
+
const threads = (yield* reviewThreads(`threads:${cycle}`, { changeRequestId })).threads.filter((thread) => !answered.has(threadKey(thread)));
|
|
21357
|
+
if (threads.length > 0) {
|
|
21358
|
+
const answers = yield* agentWork(`comments:${cycle}`, {
|
|
21359
|
+
agent: "implementer",
|
|
21360
|
+
prompt: repliesPrompt(threads),
|
|
21361
|
+
output: Replies,
|
|
21362
|
+
timeout: stepBudget
|
|
21363
|
+
});
|
|
21364
|
+
for (const answer of answers.replies) {
|
|
21365
|
+
yield* comment(`reply:${cycle}:${answer.threadId}`, {
|
|
21366
|
+
changeRequestId,
|
|
21367
|
+
threadId: answer.threadId,
|
|
21368
|
+
body: answer.reply
|
|
21369
|
+
});
|
|
21370
|
+
replies++;
|
|
21371
|
+
}
|
|
21372
|
+
for (const thread of threads)
|
|
21373
|
+
answered.add(`${thread.id}:${thread.comments.length + 1}`);
|
|
21374
|
+
if (answers.replies.some((answer) => answer.changed))
|
|
21375
|
+
continue;
|
|
21376
|
+
}
|
|
21377
|
+
const commented = {
|
|
21378
|
+
event: "change_request.comment",
|
|
21379
|
+
filter: { changeRequestId },
|
|
21380
|
+
...pipeline.readAt === undefined ? undefined : { since: pipeline.readAt }
|
|
21381
|
+
};
|
|
21382
|
+
const mergeable = yield* waitFor(`mergeable:${cycle}`, {
|
|
21383
|
+
event: "change_request.mergeable",
|
|
21384
|
+
filter: { changeRequestId },
|
|
21385
|
+
timeout: "1 hour",
|
|
21386
|
+
or: [commented]
|
|
21387
|
+
});
|
|
21388
|
+
if (mergeable.type === commented.event)
|
|
21389
|
+
continue;
|
|
21390
|
+
if (mergeable.payload.mergeable === false) {
|
|
21391
|
+
const synced = yield* syncBase(`sync:${cycle}`, { agent: "implementer", target });
|
|
21392
|
+
if (synced.status === "conflict")
|
|
21393
|
+
return done("needs_you", `Merging ${target} in conflicts on ${synced.files.join(", ")}: ${synced.reason}`);
|
|
21394
|
+
if (synced.status === "merged") {
|
|
21395
|
+
const caught = synced.catchUp;
|
|
21396
|
+
const verdict = caught === undefined ? { kind: "changed", why: "its Runner did not report what it did to the branch" } : catchUpVerdict(caught);
|
|
21397
|
+
if (verdict.kind === "changed") {
|
|
21398
|
+
unreviewed = undefined;
|
|
21399
|
+
notCarried = `merging ${target} in changed it: ${verdict.why}`;
|
|
21400
|
+
} else if (caught !== undefined && unreviewed !== undefined) {
|
|
21401
|
+
const files = [...new Set([...unreviewed.files, ...verdict.kind === "review" ? verdict.files : []])];
|
|
21402
|
+
unreviewed = caught.from === unreviewed.synced ? { ...unreviewed, files, synced: caught.head } : { ...unreviewed, files };
|
|
21403
|
+
} else if (caught !== undefined && verdict.kind === "review") {
|
|
21404
|
+
notCarried = undefined;
|
|
21405
|
+
unreviewed = { files: verdict.files, synced: caught.head, sync: `sync:${cycle}`, earlier: [] };
|
|
21406
|
+
} else if (caught !== undefined) {
|
|
21407
|
+
notCarried = undefined;
|
|
21408
|
+
const carried = yield* approveChangeRequest(`carry:${cycle}`, {
|
|
21409
|
+
changeRequestId,
|
|
21410
|
+
head: caught.head,
|
|
21411
|
+
summary: `Merging ${target} in left the branch's own diff as it was approved.`,
|
|
21412
|
+
carries: `sync:${cycle}`
|
|
21413
|
+
}).pipe(catchTag2("StepFailed", (error) => succeed7({ approved: false, reason: error.reason })));
|
|
21414
|
+
if (!carried.approved)
|
|
21415
|
+
notCarried = `the approval was not carried: ${carried.reason}`;
|
|
21416
|
+
}
|
|
21417
|
+
}
|
|
21418
|
+
continue;
|
|
21419
|
+
}
|
|
21420
|
+
let reviewedHere = false;
|
|
21421
|
+
if (unreviewed !== undefined) {
|
|
21422
|
+
const files = unreviewed;
|
|
21423
|
+
const head = pipeline.head;
|
|
21424
|
+
const reviewed = yield* call(`review:${cycle}`, reviewLoop(), {
|
|
21425
|
+
base: `origin/${target}`,
|
|
21426
|
+
head,
|
|
21427
|
+
files: files.files,
|
|
21428
|
+
...head === files.synced ? undefined : { since: files.synced },
|
|
21429
|
+
...files.earlier.length === 0 ? undefined : { earlier: files.earlier }
|
|
21430
|
+
});
|
|
21431
|
+
if (reviewed.blocking) {
|
|
21432
|
+
const blocking = reviewed.findings.filter(isBlocking);
|
|
21433
|
+
if (fixRounds >= (input.maxFixRounds ?? 3))
|
|
21434
|
+
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`);
|
|
21435
|
+
fixRounds++;
|
|
21436
|
+
yield* agentWork(`review-fix:${cycle}`, {
|
|
21437
|
+
agent: "implementer",
|
|
21438
|
+
prompt: reviewFixPrompt(blocking),
|
|
21439
|
+
output: ReviewFix,
|
|
21440
|
+
timeout: stepBudget
|
|
21441
|
+
});
|
|
21442
|
+
unreviewed = { ...files, earlier: reviewed.findings };
|
|
21443
|
+
continue;
|
|
21444
|
+
}
|
|
21445
|
+
const approval = yield* approveChangeRequest(`approve:${cycle}`, {
|
|
21446
|
+
changeRequestId,
|
|
21447
|
+
head,
|
|
21448
|
+
summary: `A review of the files merging ${target} in changed (${files.files.join(", ")}) passed with nothing blocking.`,
|
|
21449
|
+
review: `review:${cycle}`,
|
|
21450
|
+
carries: files.sync
|
|
21451
|
+
}).pipe(catchTag2("StepFailed", (error) => succeed7({ approved: false, reason: error.reason })));
|
|
21452
|
+
unreviewed = undefined;
|
|
21453
|
+
if (approval.approved)
|
|
21454
|
+
reviewedHere = true;
|
|
21455
|
+
else
|
|
21456
|
+
notCarried = `merging ${target} in, whose files' review passed but did not approve it: ${approval.reason}`;
|
|
21457
|
+
}
|
|
21458
|
+
const moved = pipeline.readAt === undefined ? [] : [
|
|
21459
|
+
{ event: "change_request.pipeline", filter: { changeRequestId }, since: pipeline.readAt },
|
|
21460
|
+
...pipeline.source === "reported" ? [
|
|
21461
|
+
{
|
|
21462
|
+
event: "change_request.approval",
|
|
21463
|
+
filter: { changeRequestId, approved: true },
|
|
21464
|
+
since: pipeline.readAt
|
|
21465
|
+
}
|
|
21466
|
+
] : []
|
|
21467
|
+
];
|
|
21468
|
+
const approved = reviewedHere ? undefined : yield* waitFor(`approved:${cycle}`, {
|
|
21469
|
+
event: "change_request.approval",
|
|
21470
|
+
filter: { changeRequestId, approved: true, head: pipeline.head },
|
|
21471
|
+
timeout: "7 days",
|
|
21472
|
+
or: [commented, ...moved]
|
|
21473
|
+
});
|
|
21474
|
+
if (approved !== undefined && approved.type !== "change_request.approval")
|
|
21475
|
+
continue;
|
|
21476
|
+
if (pipeline.source === "reported" && approved !== undefined && approved.payload.head !== pipeline.head)
|
|
21477
|
+
continue;
|
|
21478
|
+
const merge = input.merge === true ? "Merge" : yield* ask(`merge:${cycle}`, {
|
|
21479
|
+
prompt: "The ChangeRequest is approved, green and mergeable. Merge now?",
|
|
21480
|
+
options: [
|
|
21481
|
+
{ value: "Merge", description: "Merges the head whose pipeline passed." },
|
|
21482
|
+
{ value: "Wait", description: "Leaves the merge to a person." }
|
|
21483
|
+
],
|
|
21484
|
+
recommended: "Merge",
|
|
21485
|
+
recommendationReason: "It is approved, green and mergeable, which is everything a merge waits for.",
|
|
21486
|
+
default: "Wait",
|
|
21487
|
+
auto: "Merge",
|
|
21488
|
+
timeout: "1 hour"
|
|
21489
|
+
});
|
|
21490
|
+
if (merge === "Wait")
|
|
21491
|
+
return done("ready", "Approved, green and mergeable; left for a person to merge");
|
|
21492
|
+
const merged = yield* mergeChangeRequest(`merge-change-request:${cycle}`, {
|
|
21493
|
+
changeRequestId,
|
|
21494
|
+
head: pipeline.head
|
|
21495
|
+
});
|
|
21496
|
+
if (merged.status !== "merged")
|
|
21497
|
+
continue;
|
|
21498
|
+
yield* writeBack("write-back", { milestone: "merged" });
|
|
21499
|
+
const ship = yield* ask("ship", {
|
|
21500
|
+
prompt: `Merged at ${merged.revision}. Release it now?`,
|
|
21501
|
+
options: [
|
|
21502
|
+
{ value: "Release", description: "Starts Release on the merge: it gates, tags, publishes and deploys it." },
|
|
21503
|
+
{ value: "Not now", description: "Leaves the release to a person or a later Release." }
|
|
21504
|
+
],
|
|
21505
|
+
recommended: "Release",
|
|
21506
|
+
recommendationReason: "The merge is approved and green, so it is ready to ship.",
|
|
21507
|
+
default: "Not now",
|
|
21508
|
+
auto: "Release",
|
|
21509
|
+
timeout: "1 hour"
|
|
21510
|
+
});
|
|
21511
|
+
if (ship === "Not now")
|
|
21512
|
+
return done("merged", `Merged at ${merged.revision}`);
|
|
21513
|
+
const released = yield* call("release", release_default, { revision: merged.revision });
|
|
21514
|
+
if (released.status !== "deployed" && released.status !== "rejected")
|
|
21515
|
+
yield* ask("release-failed", {
|
|
21516
|
+
prompt: `Merged at ${merged.revision}, but its Release ended ${released.status}: ${released.reason}`,
|
|
21517
|
+
options: [{ value: "Taken over", description: "A person looks at the release and production from here." }],
|
|
21518
|
+
recommended: "Taken over",
|
|
21519
|
+
recommendationReason: "The merge is in, but production may not run it, or may be broken.",
|
|
21520
|
+
default: null
|
|
21521
|
+
});
|
|
21522
|
+
return {
|
|
21523
|
+
...done("merged", `Merged at ${merged.revision}; Release: ${released.reason}`),
|
|
21524
|
+
release: {
|
|
21525
|
+
runStatus: released.status,
|
|
21526
|
+
reason: released.reason,
|
|
21527
|
+
...released.tag === undefined ? undefined : { tag: released.tag }
|
|
21528
|
+
}
|
|
21529
|
+
};
|
|
21530
|
+
}
|
|
21531
|
+
}).pipe(catchIf2((error) => error._tag === "StepFailed" && error.reason.includes("time budget"), (error) => succeed7(done("needs_you", `Step "${error.step}" ran past its ${stepBudget} budget`))), catchTags2({
|
|
21532
|
+
WaitTimedOut: (timedOut) => succeed7(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}`)),
|
|
21533
|
+
ChangeRequestClosed: (ended) => succeed7(ended.merged ? done("merged", "Someone else merged the ChangeRequest") : done("needs_you", "The ChangeRequest was closed"))
|
|
21534
|
+
}));
|
|
21535
|
+
}
|
|
21492
21536
|
});
|
|
21493
21537
|
|
|
21494
|
-
// src/
|
|
21495
|
-
var
|
|
21496
|
-
words: String5,
|
|
21538
|
+
// src/build.ts
|
|
21539
|
+
var BuildInput = Struct({
|
|
21540
|
+
words: optional2(String5),
|
|
21497
21541
|
taskId: optional2(String5),
|
|
21498
21542
|
repoBindingId: optional2(String5),
|
|
21499
|
-
branch: optional2(String5)
|
|
21500
|
-
|
|
21501
|
-
|
|
21502
|
-
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
checks: optional2(ArraySchema(SuggestedCheck))
|
|
21507
|
-
});
|
|
21508
|
-
var budget = "1 hour";
|
|
21509
|
-
var Output3 = Struct({
|
|
21510
|
-
status: Literals(["done", "blocked", "too_big", "opened", "handed_to_build", "discarded"]),
|
|
21511
|
-
summary: String5,
|
|
21512
|
-
changeRequest: optional2(ChangeRequestResult)
|
|
21513
|
-
});
|
|
21514
|
-
var prompt2 = (words) => [
|
|
21515
|
-
`Just do this: ${words}`,
|
|
21516
|
-
implementAdaptation,
|
|
21517
|
-
"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.",
|
|
21518
|
-
"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.",
|
|
21519
|
-
"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."
|
|
21520
|
-
].join(`
|
|
21521
|
-
|
|
21522
|
-
`);
|
|
21523
|
-
var errand_default = defineLoop({
|
|
21524
|
-
id: "errand",
|
|
21525
|
-
version: "1.0.0",
|
|
21526
|
-
title: "Errand",
|
|
21527
|
-
input: Input3,
|
|
21528
|
-
output: Output3,
|
|
21529
|
-
agents: { implementer: "implementer" },
|
|
21530
|
-
checkout: "write",
|
|
21531
|
-
offers: [{ id: "build", title: "Build it with a full review", loop: "build" }],
|
|
21532
|
-
capabilities: ["agent_work", "verify", "change_request", "write_back", "ask"],
|
|
21533
|
-
calls: () => [build_default],
|
|
21534
|
-
run: (input) => gen2(function* () {
|
|
21535
|
-
const done = yield* agentWorkOnce("errand", {
|
|
21536
|
-
agent: "implementer",
|
|
21537
|
-
skill: "implement",
|
|
21538
|
-
prompt: prompt2(input.words),
|
|
21539
|
-
output: Done3,
|
|
21540
|
-
timeout: budget
|
|
21541
|
-
}).pipe(catchIf2((error) => error._tag === "StepFailed" && error.reason.includes("time budget"), () => succeed7({
|
|
21542
|
-
status: "too_big",
|
|
21543
|
-
summary: `It took longer than an errand's ${budget}, so it is a feature: Build can do it with a full review.`,
|
|
21544
|
-
changed: false
|
|
21545
|
-
})));
|
|
21546
|
-
if (done.status !== "done" || !done.changed)
|
|
21547
|
-
return { status: done.status, summary: done.summary };
|
|
21548
|
-
const head = yield* verify("verify", { suggested: done.checks ?? [] });
|
|
21549
|
-
const answer = yield* ask("next", {
|
|
21550
|
-
prompt: `${done.summary}
|
|
21551
|
-
|
|
21552
|
-
The checks ${head.passed ? "pass" : "fail"}. What next?`,
|
|
21553
|
-
options: [
|
|
21554
|
-
{ value: "Open a ChangeRequest", description: "Deliver it as it is, unreviewed." },
|
|
21555
|
-
{ value: "Hand to Build for a full review", description: "Build reviews and verifies it before delivering." },
|
|
21556
|
-
{ value: "Discard", description: "Drop the change." }
|
|
21557
|
-
],
|
|
21558
|
-
recommended: head.passed ? "Open a ChangeRequest" : "Hand to Build for a full review",
|
|
21559
|
-
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.",
|
|
21560
|
-
default: "Open a ChangeRequest",
|
|
21561
|
-
timeout: "1 hour"
|
|
21562
|
-
});
|
|
21563
|
-
if (answer === "Discard")
|
|
21564
|
-
return { status: "discarded", summary: done.summary };
|
|
21565
|
-
if (answer === "Hand to Build for a full review") {
|
|
21566
|
-
const built = yield* call("build", build_default, {
|
|
21567
|
-
words: input.words,
|
|
21568
|
-
revision: head.revision,
|
|
21569
|
-
...input.taskId === undefined ? undefined : { taskId: input.taskId },
|
|
21570
|
-
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
21571
|
-
});
|
|
21572
|
-
return { status: "handed_to_build", summary: done.summary, changeRequest: built.changeRequest };
|
|
21573
|
-
}
|
|
21574
|
-
const opened = yield* changeRequest("change-request", {
|
|
21575
|
-
title: done.title ?? input.words.slice(0, 120),
|
|
21576
|
-
body: [
|
|
21577
|
-
done.summary,
|
|
21578
|
-
"> **Unreviewed errand.** One agent did this with no review; ask for a Build or a Review if it needs one.",
|
|
21579
|
-
"## Evidence",
|
|
21580
|
-
evidenceTable(head)
|
|
21581
|
-
].join(`
|
|
21582
|
-
|
|
21583
|
-
`)
|
|
21584
|
-
});
|
|
21585
|
-
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
21586
|
-
return { status: "opened", summary: done.summary, changeRequest: opened };
|
|
21587
|
-
})
|
|
21588
|
-
});
|
|
21589
|
-
|
|
21590
|
-
// src/file-tasks.ts
|
|
21591
|
-
var FileTasksInput = Struct({
|
|
21592
|
-
tickets: PlanTickets,
|
|
21593
|
-
projectId: optional2(String5)
|
|
21594
|
-
});
|
|
21595
|
-
var Output4 = Struct({
|
|
21596
|
-
tasks: ArraySchema(Struct({ key: String5, taskId: String5 }))
|
|
21543
|
+
branch: optional2(String5),
|
|
21544
|
+
revision: optional2(String5),
|
|
21545
|
+
outcome: optional2(LoopOutcome),
|
|
21546
|
+
maxRounds: optional2(Int),
|
|
21547
|
+
continuing: optional2(Continuing),
|
|
21548
|
+
land: optional2(Boolean3),
|
|
21549
|
+
changeRequestId: optional2(String5)
|
|
21597
21550
|
});
|
|
21598
|
-
var
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
input: FileTasksInput,
|
|
21603
|
-
output: Output4,
|
|
21604
|
-
agents: {},
|
|
21605
|
-
checkout: "none",
|
|
21606
|
-
capabilities: ["create_tasks"],
|
|
21607
|
-
run: (input) => createTasks("tasks", {
|
|
21608
|
-
tickets: input.tickets,
|
|
21609
|
-
...input.projectId === undefined ? undefined : { projectId: input.projectId }
|
|
21610
|
-
}).pipe(map8(({ tasks }) => ({ tasks })))
|
|
21551
|
+
var CriterionAnswer = Struct({
|
|
21552
|
+
criterion: String5,
|
|
21553
|
+
status: Literals(["met", "after_merge", "not_met"]),
|
|
21554
|
+
detail: String5
|
|
21611
21555
|
});
|
|
21612
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
repoBindingId: optional2(String5),
|
|
21618
|
-
branch: optional2(String5),
|
|
21619
|
-
cycles: optional2(Int),
|
|
21620
|
-
threshold: optional2(Finite),
|
|
21621
|
-
tolerance: optional2(Finite)
|
|
21556
|
+
var Criteria = Struct({ criteria: ArraySchema(CriterionAnswer) });
|
|
21557
|
+
var SuggestedCheck = Struct({
|
|
21558
|
+
name: CheckName,
|
|
21559
|
+
command: String5,
|
|
21560
|
+
workingDirectory: optionalKey2(String5)
|
|
21622
21561
|
});
|
|
21623
|
-
var
|
|
21562
|
+
var Implemented = Struct({
|
|
21624
21563
|
title: String5,
|
|
21625
|
-
|
|
21626
|
-
|
|
21627
|
-
|
|
21564
|
+
summary: String5,
|
|
21565
|
+
claims: ArraySchema(String5),
|
|
21566
|
+
documented: optional2(ArraySchema(String5)),
|
|
21567
|
+
rollback: optional2(String5),
|
|
21568
|
+
criteria: optional2(ArraySchema(CriterionAnswer)),
|
|
21569
|
+
checks: optional2(ArraySchema(SuggestedCheck))
|
|
21628
21570
|
});
|
|
21629
|
-
var
|
|
21630
|
-
|
|
21631
|
-
|
|
21632
|
-
|
|
21633
|
-
|
|
21571
|
+
var Reproduced = Struct({
|
|
21572
|
+
check: String5,
|
|
21573
|
+
summary: String5
|
|
21574
|
+
});
|
|
21575
|
+
var ReproducedOnBranch = Struct({
|
|
21576
|
+
...Reproduced.fields,
|
|
21577
|
+
revision: optional2(String5)
|
|
21634
21578
|
});
|
|
21635
|
-
var
|
|
21579
|
+
var Fixed = Struct({
|
|
21636
21580
|
summary: String5,
|
|
21637
|
-
|
|
21638
|
-
|
|
21639
|
-
var Change = Struct({
|
|
21640
|
-
status: Literals(["changed", "exhausted", "unsafe"]),
|
|
21641
|
-
summary: String5
|
|
21581
|
+
claims: ArraySchema(String5),
|
|
21582
|
+
rebuttals: ArraySchema(Rebuttal)
|
|
21642
21583
|
});
|
|
21643
|
-
var
|
|
21644
|
-
|
|
21645
|
-
|
|
21646
|
-
|
|
21647
|
-
|
|
21648
|
-
|
|
21649
|
-
|
|
21650
|
-
|
|
21651
|
-
|
|
21652
|
-
|
|
21584
|
+
var BuildOutput = Struct({
|
|
21585
|
+
changeRequest: ChangeRequestResult,
|
|
21586
|
+
revision: String5,
|
|
21587
|
+
tree: String5,
|
|
21588
|
+
settled: Boolean3,
|
|
21589
|
+
unsettled: ArraySchema(String5),
|
|
21590
|
+
rounds: Int,
|
|
21591
|
+
stopped: Literals(["settled", "dispute_stands", "findings_returned", "round_cap"]),
|
|
21592
|
+
criteria: ArraySchema(CriterionAnswer),
|
|
21593
|
+
approval: optional2(ApproveChangeRequestResult),
|
|
21594
|
+
landed: optional2(ShepherdOutput),
|
|
21595
|
+
next: Struct({ changeRequestId: String5 })
|
|
21653
21596
|
});
|
|
21654
|
-
var discoverPrompt = (focus) => [
|
|
21655
|
-
`Find where this repository is slow${focus === undefined ? ", for the journeys that matter most" : `: ${focus}`}. Change nothing.`,
|
|
21656
|
-
"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.",
|
|
21657
|
-
"Rank the opportunities, most promising first, each marked measured, telemetry or hypothesis. Mark one that only another repository can make as external."
|
|
21658
|
-
].join(`
|
|
21659
21597
|
|
|
21660
|
-
|
|
21661
|
-
|
|
21662
|
-
|
|
21663
|
-
|
|
21664
|
-
|
|
21665
|
-
|
|
21666
|
-
|
|
21598
|
+
class BuildTargetMissing extends TaggedError3()("BuildTargetMissing", {
|
|
21599
|
+
message: String5
|
|
21600
|
+
}) {
|
|
21601
|
+
}
|
|
21602
|
+
var outcomeGuidance = {
|
|
21603
|
+
feature: "Deliver the whole scope; review checks it was met.",
|
|
21604
|
+
bug: "Your failing test is already in place: make it pass.",
|
|
21605
|
+
refactor: "Keep behaviour exactly as it is; the approved verifications must pass before and after.",
|
|
21606
|
+
investigation: "Investigate and report what you found, with its sources.",
|
|
21607
|
+
docs: "Name, in `documented`, the approved checks that run the commands you documented.",
|
|
21608
|
+
migration: "Name, in `rollback`, the approved check that rolls the migration back.",
|
|
21609
|
+
performance: "Make it faster without changing behaviour."
|
|
21610
|
+
};
|
|
21667
21611
|
|
|
21668
|
-
|
|
21669
|
-
|
|
21670
|
-
|
|
21671
|
-
|
|
21672
|
-
|
|
21673
|
-
|
|
21674
|
-
|
|
21612
|
+
class BuildOutcomeRefused extends TaggedError3()("BuildOutcomeRefused", {
|
|
21613
|
+
message: String5
|
|
21614
|
+
}) {
|
|
21615
|
+
}
|
|
21616
|
+
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.";
|
|
21617
|
+
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.";
|
|
21618
|
+
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.";
|
|
21619
|
+
var implementPrompt = (task, outcome, continuing) => [
|
|
21620
|
+
...continuing === undefined ? [] : [continuingBrief(continuing), "Fix forward from there."],
|
|
21621
|
+
`Build ${task}`,
|
|
21622
|
+
implementAdaptation,
|
|
21623
|
+
...outcome === undefined ? [] : [outcomeGuidance[outcome]],
|
|
21624
|
+
'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".',
|
|
21625
|
+
criteriaGuidance,
|
|
21626
|
+
checksGuidance
|
|
21675
21627
|
].join(`
|
|
21676
21628
|
|
|
21677
21629
|
`);
|
|
21678
|
-
var
|
|
21679
|
-
|
|
21680
|
-
|
|
21681
|
-
|
|
21630
|
+
var criteriaPrompt = (unanswered, unmet) => [
|
|
21631
|
+
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:",
|
|
21632
|
+
...unmet.map((answer) => `- ${answer.criterion}: ${answer.detail}`),
|
|
21633
|
+
criteriaGuidance
|
|
21682
21634
|
].join(`
|
|
21683
21635
|
|
|
21684
21636
|
`);
|
|
21685
|
-
var
|
|
21686
|
-
var
|
|
21687
|
-
|
|
21688
|
-
"
|
|
21689
|
-
...measured.metrics.map((metric) => `| ${metric.name} | ${metric.role} | ${metric.base.median} | ${metric.head.median} | ${percent(metric.change)} | ${percent(metric.noise)} |`)
|
|
21637
|
+
var answerLine = (answer) => `${answer.criterion}: ${answer.status.replaceAll("_", " ")}, ${answer.detail}`;
|
|
21638
|
+
var reproducePrompt = (task) => [
|
|
21639
|
+
`Reproduce the bug in ${task} before anyone fixes it.`,
|
|
21640
|
+
"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."
|
|
21690
21641
|
].join(`
|
|
21691
|
-
`);
|
|
21692
|
-
var report = (input) => [
|
|
21693
|
-
`# Hillclimb findings
|
|
21694
|
-
|
|
21695
|
-
${input.reason}`,
|
|
21696
|
-
`## Stack
|
|
21697
|
-
|
|
21698
|
-
${input.discovery.stack}`,
|
|
21699
|
-
`## Telemetry
|
|
21700
|
-
|
|
21701
|
-
${input.discovery.telemetry.map((entry) => `- ${entry.source}: ${entry.status}, ${entry.why}`).join(`
|
|
21702
|
-
`) || "- None reached"}`,
|
|
21703
|
-
`## Benchmark
|
|
21704
|
-
|
|
21705
|
-
${input.harness.summary}`,
|
|
21706
|
-
`## Tried
|
|
21707
21642
|
|
|
21708
|
-
|
|
21709
|
-
|
|
21710
|
-
|
|
21711
|
-
|
|
21712
|
-
|
|
21713
|
-
`)}`],
|
|
21714
|
-
...input.final === undefined ? [] : [`## Final measurement
|
|
21715
|
-
|
|
21716
|
-
${input.final.reason}
|
|
21717
|
-
|
|
21718
|
-
${numbers(input.final)}`],
|
|
21719
|
-
`## Opportunities
|
|
21720
|
-
|
|
21721
|
-
${input.discovery.opportunities.map((entry) => `- ${entry.title} (${entry.evidence}${entry.external === true ? ", external" : ""}): ${entry.detail}`).join(`
|
|
21722
|
-
`)}`
|
|
21643
|
+
`);
|
|
21644
|
+
var reproducedOnBranchPrompt = (task) => [
|
|
21645
|
+
`An earlier Run already worked on fixing the bug in ${task}, on this branch.`,
|
|
21646
|
+
"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.",
|
|
21647
|
+
"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`."
|
|
21723
21648
|
].join(`
|
|
21724
21649
|
|
|
21725
21650
|
`);
|
|
21726
|
-
var
|
|
21727
|
-
|
|
21728
|
-
|
|
21729
|
-
|
|
21730
|
-
|
|
21731
|
-
|
|
21732
|
-
|
|
21733
|
-
|
|
21734
|
-
|
|
21651
|
+
var findingLine = (finding) => `- **${finding.severity}** \`${finding.id}\` ${finding.detail}`;
|
|
21652
|
+
var disputeLine = ({ finding, rebuttal }) => `${findingLine(finding)}
|
|
21653
|
+
You rebutted: ${rebuttal.reason}`;
|
|
21654
|
+
var fixPrompt = (settled) => {
|
|
21655
|
+
const checks = settled.open.filter((finding) => isCheckFinding(finding.id));
|
|
21656
|
+
const findings = settled.open.filter((finding) => !isCheckFinding(finding.id));
|
|
21657
|
+
return [
|
|
21658
|
+
...checks.length === 0 ? [] : [
|
|
21659
|
+
"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.",
|
|
21660
|
+
...checks.map(findingLine)
|
|
21661
|
+
],
|
|
21662
|
+
...findings.length === 0 ? [] : [
|
|
21663
|
+
"The review found these. Fix each, or rebut it with your reason if it is wrong; reviewers judge the rebuttals next round.",
|
|
21664
|
+
...findings.map(findingLine)
|
|
21665
|
+
],
|
|
21666
|
+
...settled.standing.length === 0 ? [] : [
|
|
21667
|
+
"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.",
|
|
21668
|
+
...settled.standing.map(disputeLine)
|
|
21669
|
+
]
|
|
21670
|
+
].join(`
|
|
21671
|
+
`);
|
|
21735
21672
|
};
|
|
21736
|
-
var
|
|
21737
|
-
|
|
21738
|
-
|
|
21739
|
-
|
|
21740
|
-
|
|
21741
|
-
|
|
21742
|
-
|
|
21743
|
-
|
|
21673
|
+
var blockingIds = (settled) => settled.open.filter(isBlocking).map((finding) => finding.id);
|
|
21674
|
+
var onlyChecksBlock = (settled) => blockingIds(settled).every(isCheckFinding) && !settled.standing.some((entry) => isBlocking(entry.finding));
|
|
21675
|
+
var rally = (input) => gen2(function* () {
|
|
21676
|
+
const claims = [...input.claims];
|
|
21677
|
+
const conflicts = [];
|
|
21678
|
+
let rebuttals = [];
|
|
21679
|
+
let earlier = [];
|
|
21680
|
+
let previous;
|
|
21681
|
+
let last;
|
|
21682
|
+
const shown = new Set;
|
|
21683
|
+
for (let round = 1;; round++) {
|
|
21684
|
+
const synced = yield* syncBase(`sync:${round}`, {
|
|
21685
|
+
agent: "implementer",
|
|
21686
|
+
...input.target === undefined ? undefined : { target: input.target }
|
|
21687
|
+
});
|
|
21688
|
+
if (synced.status === "conflict")
|
|
21689
|
+
conflicts.push(`Could not merge the target branch in round ${round}: ${synced.files.join(", ")} (${synced.reason})`);
|
|
21690
|
+
const head = yield* verify(`verify:${round}`, { suggested: input.suggested });
|
|
21691
|
+
if (last !== undefined && last.head.tree === head.tree && rebuttals.length === 0 && onlyChecksBlock(last.settled))
|
|
21692
|
+
return {
|
|
21693
|
+
...last,
|
|
21694
|
+
round: round - 1,
|
|
21695
|
+
head,
|
|
21696
|
+
decision: { stop: true, reason: "findings_returned", unsettled: blockingIds(last.settled) },
|
|
21697
|
+
claims,
|
|
21698
|
+
conflicts
|
|
21699
|
+
};
|
|
21700
|
+
const failing = brokenChecks(head).map((check) => check.name);
|
|
21701
|
+
const reviewed = yield* call(`review:${round}`, review_default, {
|
|
21702
|
+
base: input.base,
|
|
21703
|
+
head: head.revision,
|
|
21704
|
+
...rebuttals.length === 0 ? undefined : { rebuttals },
|
|
21705
|
+
...earlier.length === 0 ? undefined : { earlier },
|
|
21706
|
+
...input.outcome === undefined ? undefined : { outcome: input.outcome },
|
|
21707
|
+
...failing.length === 0 ? undefined : { failing },
|
|
21708
|
+
claims: [...claims]
|
|
21709
|
+
});
|
|
21710
|
+
const checks = checkFindings(head);
|
|
21711
|
+
const settled = settleRound({
|
|
21712
|
+
findings: [...reviewed.findings, ...scopeFindings(reviewed.scope), ...checks],
|
|
21713
|
+
rebuttals,
|
|
21714
|
+
verdicts: reviewed.verdicts
|
|
21715
|
+
});
|
|
21716
|
+
earlier = [...settled.open, ...settled.standing.map((entry) => entry.finding)].filter((finding) => !finding.id.startsWith("scope: ") && !isCheckFinding(finding.id));
|
|
21717
|
+
const unseen = checks.some((finding) => !shown.has(finding.detail));
|
|
21718
|
+
const decision = rallyStop({
|
|
21719
|
+
round,
|
|
21720
|
+
maxRounds: input.maxRounds,
|
|
21721
|
+
settled,
|
|
21722
|
+
...previous === undefined || unseen ? undefined : { previous }
|
|
21723
|
+
});
|
|
21724
|
+
if (decision.stop && (!unseen || round >= input.maxRounds))
|
|
21725
|
+
return { round, head, settled, decision, claims, scope: reviewed.scope, conflicts };
|
|
21726
|
+
for (const finding of checks)
|
|
21727
|
+
shown.add(finding.detail);
|
|
21728
|
+
const fixed = yield* agentWork(`fix:${round}`, {
|
|
21729
|
+
agent: "implementer",
|
|
21730
|
+
prompt: fixPrompt(settled),
|
|
21731
|
+
output: Fixed
|
|
21732
|
+
});
|
|
21733
|
+
claims.push(...fixed.claims, ...fixed.rebuttals.flatMap((rebuttal) => isCheckFinding(rebuttal.findingId) ? [`Rebutted ${rebuttal.findingId}: ${rebuttal.reason}`] : []));
|
|
21734
|
+
const rebutted = fixed.rebuttals.filter((rebuttal) => !isCheckFinding(rebuttal.findingId));
|
|
21735
|
+
rebuttals = [
|
|
21736
|
+
...rebutted,
|
|
21737
|
+
...settled.standing.filter((entry) => isBlocking(entry.finding)).map((entry) => entry.rebuttal).filter((rebuttal) => !rebutted.some((entry) => entry.findingId === rebuttal.findingId))
|
|
21738
|
+
];
|
|
21739
|
+
previous = blockingIds(settled);
|
|
21740
|
+
last = { head, settled, scope: reviewed.scope };
|
|
21741
|
+
}
|
|
21742
|
+
});
|
|
21743
|
+
var build_default = defineLoop({
|
|
21744
|
+
id: "build",
|
|
21745
|
+
version: "2.2.0",
|
|
21746
|
+
title: "Build",
|
|
21747
|
+
input: BuildInput,
|
|
21748
|
+
output: BuildOutput,
|
|
21749
|
+
agents: { implementer: "implementer" },
|
|
21744
21750
|
checkout: "write",
|
|
21745
|
-
|
|
21746
|
-
|
|
21747
|
-
|
|
21751
|
+
offers: [
|
|
21752
|
+
{ id: "shepherd", title: "Shepherd it to merge", loop: "shepherd", from: "next" },
|
|
21753
|
+
{ id: "review", title: "Review again", loop: "review", from: "next" },
|
|
21754
|
+
{ id: "tidy", title: "Tidy", loop: "tidy" },
|
|
21755
|
+
{ id: "retry", title: "Retry with a steer", loop: "build" }
|
|
21756
|
+
],
|
|
21757
|
+
capabilities: ["agent_work", "verify", "change_request", "write_back", "post_comment", "approve_change_request"],
|
|
21758
|
+
calls: () => [review_default, shepherd_default],
|
|
21748
21759
|
run: (input) => gen2(function* () {
|
|
21749
|
-
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
const
|
|
21759
|
-
|
|
21760
|
+
if (input.words === undefined && input.taskId === undefined)
|
|
21761
|
+
return yield* new BuildTargetMissing({ message: "Give the words to build, or a Task" });
|
|
21762
|
+
if (input.outcome === "performance")
|
|
21763
|
+
return yield* new BuildOutcomeRefused({
|
|
21764
|
+
message: "Build cannot prove a performance gain; start Hillclimb, which commits a benchmark and keeps only the changes Kitman measures faster"
|
|
21765
|
+
});
|
|
21766
|
+
yield* writeBack("write-back:started", { milestone: "started" });
|
|
21767
|
+
const task = input.words === undefined ? "the Task your brief describes." : `this: ${input.words}`;
|
|
21768
|
+
const outcome = input.outcome;
|
|
21769
|
+
const target = input.branch ?? input.continuing?.targetBranch;
|
|
21770
|
+
if (input.changeRequestId !== undefined && input.continuing?.changeRequestId !== input.changeRequestId)
|
|
21771
|
+
return yield* new BuildTargetMissing({
|
|
21772
|
+
message: `Kitman did not put this Run on ChangeRequest ${input.changeRequestId}'s branch`
|
|
21773
|
+
});
|
|
21774
|
+
const reproduced = outcome === "bug" ? yield* agentWork("reproduce", {
|
|
21760
21775
|
agent: "implementer",
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
21776
|
+
prompt: input.continuing === undefined ? reproducePrompt(task) : reproducedOnBranchPrompt(task),
|
|
21777
|
+
output: ReproducedOnBranch
|
|
21778
|
+
}) : undefined;
|
|
21779
|
+
const base = reproduced !== undefined ? yield* verify("verify:reproduction", reproduced.revision === undefined ? {} : { revision: reproduced.revision }) : outcome === "refactor" ? yield* verify("verify:base", {}) : undefined;
|
|
21780
|
+
const implemented = yield* agentWorkOnce("implement", {
|
|
21764
21781
|
agent: "implementer",
|
|
21765
|
-
|
|
21766
|
-
|
|
21767
|
-
|
|
21768
|
-
const baseline = (yield* verify("verify:harness", {})).revision;
|
|
21769
|
-
let plan = harness.plan;
|
|
21770
|
-
const calibrated = yield* measure("calibrate", { plan, base: baseline, head: baseline, ...limits });
|
|
21771
|
-
const kept = [];
|
|
21772
|
-
const tried = [];
|
|
21773
|
-
let cycles = 0;
|
|
21774
|
-
const ending = (status, reason, final) => gen2(function* () {
|
|
21775
|
-
const stored = yield* artifact("report", {
|
|
21776
|
-
kind: "hillclimb-report",
|
|
21777
|
-
title: "Hillclimb findings",
|
|
21778
|
-
text: report({ discovery, harness, reason, kept, tried, final })
|
|
21779
|
-
});
|
|
21780
|
-
return { status, reason, kept, cycles, reportArtifactId: stored.artifactId, external };
|
|
21782
|
+
skill: "implement",
|
|
21783
|
+
prompt: implementPrompt(task, outcome, input.continuing),
|
|
21784
|
+
output: Implemented
|
|
21781
21785
|
});
|
|
21782
|
-
|
|
21783
|
-
|
|
21784
|
-
|
|
21785
|
-
return yield* ending("too_noisy", `${calibrated.reason}. ${calibrated.suggestion ?? ""}`.trim());
|
|
21786
|
-
plan = switched;
|
|
21787
|
-
}
|
|
21788
|
-
let start = baseline;
|
|
21789
|
-
let unkept = 0;
|
|
21790
|
-
for (let cycle = 1;cycle <= (input.cycles ?? 5) && unkept < 2; cycle++) {
|
|
21791
|
-
const change = yield* agentWorkOnce(`change:${cycle}`, {
|
|
21792
|
-
agent: "implementer",
|
|
21793
|
-
prompt: changePrompt(discovery, plan, tried, cycle),
|
|
21794
|
-
output: Change
|
|
21795
|
-
});
|
|
21796
|
-
if (change.status !== "changed") {
|
|
21797
|
-
tried.push(`Cycle ${cycle}: ${change.status}, ${change.summary}`);
|
|
21798
|
-
break;
|
|
21799
|
-
}
|
|
21800
|
-
cycles = cycle;
|
|
21801
|
-
const head = yield* verify(`verify:${cycle}`, {});
|
|
21802
|
-
const measured = head.passed ? yield* measure(`measure:${cycle}`, { plan, base: start, head: head.revision, ...limits }) : undefined;
|
|
21803
|
-
if (measured?.verdict === "gain") {
|
|
21804
|
-
kept.push({ summary: change.summary, verdict: measured.verdict, reason: measured.reason });
|
|
21805
|
-
tried.push(`Cycle ${cycle}, kept: ${change.summary} (${measured.reason})`);
|
|
21806
|
-
start = head.revision;
|
|
21807
|
-
unkept = 0;
|
|
21808
|
-
continue;
|
|
21809
|
-
}
|
|
21810
|
-
tried.push(`Cycle ${cycle}, reverted: ${change.summary} (${measured === undefined ? "its checks failed" : measured.reason})`);
|
|
21811
|
-
yield* agentWork(`revert:${cycle}`, {
|
|
21812
|
-
agent: "implementer",
|
|
21813
|
-
prompt: revertPrompt(start, cycle),
|
|
21814
|
-
output: Struct({ reverted: Boolean3 })
|
|
21815
|
-
});
|
|
21816
|
-
unkept++;
|
|
21817
|
-
}
|
|
21818
|
-
if (external.length > 0) {
|
|
21819
|
-
const file = yield* ask("external", {
|
|
21820
|
-
prompt: `Only other repositories can make these improvements: ${external.join("; ")}. File them as Tasks?`,
|
|
21821
|
-
options: ["File as Tasks", "Leave them"],
|
|
21822
|
-
recommended: "File as Tasks",
|
|
21823
|
-
recommendationReason: "Filed as Tasks, the improvements wait in the repositories that can make them instead of being lost.",
|
|
21824
|
-
default: "Leave them",
|
|
21825
|
-
timeout: "1 hour"
|
|
21826
|
-
});
|
|
21827
|
-
if (file === "File as Tasks")
|
|
21828
|
-
yield* createTasks("external-tasks", {
|
|
21829
|
-
tickets: discovery.opportunities.flatMap((entry, index) => entry.external === true ? [
|
|
21830
|
-
{
|
|
21831
|
-
key: `external-${index + 1}`,
|
|
21832
|
-
title: entry.title,
|
|
21833
|
-
body: entry.detail,
|
|
21834
|
-
acceptanceCriteria: [],
|
|
21835
|
-
blockedBy: [],
|
|
21836
|
-
outcome: "performance"
|
|
21837
|
-
}
|
|
21838
|
-
] : [])
|
|
21839
|
-
});
|
|
21840
|
-
}
|
|
21841
|
-
if (kept.length === 0)
|
|
21842
|
-
return yield* ending("no_gain", "No change proved faster than the noise allows");
|
|
21843
|
-
const synced = yield* syncBase("sync:final", {
|
|
21786
|
+
const unanswered = input.taskId !== undefined && implemented.criteria === undefined;
|
|
21787
|
+
const unmet = (implemented.criteria ?? []).filter((answer) => answer.status === "not_met");
|
|
21788
|
+
const criteria = unanswered || unmet.length > 0 ? (yield* agentWorkOnce("criteria", {
|
|
21844
21789
|
agent: "implementer",
|
|
21845
|
-
|
|
21790
|
+
prompt: criteriaPrompt(unanswered, unmet),
|
|
21791
|
+
output: Criteria
|
|
21792
|
+
})).criteria : implemented.criteria ?? [];
|
|
21793
|
+
const last = yield* rally({
|
|
21794
|
+
maxRounds: input.maxRounds ?? 3,
|
|
21795
|
+
base: `origin/${target ?? "HEAD"}`,
|
|
21796
|
+
target,
|
|
21797
|
+
outcome,
|
|
21798
|
+
claims: [...implemented.claims, ...criteria.map((answer) => `Criterion ${answerLine(answer)}`)],
|
|
21799
|
+
suggested: implemented.checks ?? []
|
|
21846
21800
|
});
|
|
21847
|
-
const
|
|
21848
|
-
|
|
21849
|
-
|
|
21850
|
-
|
|
21851
|
-
|
|
21852
|
-
|
|
21853
|
-
|
|
21854
|
-
|
|
21855
|
-
const claims = [
|
|
21856
|
-
"Behaviour is preserved: every kept change leaves what the code does exactly as it was.",
|
|
21857
|
-
`The measurement is valid: ${harness.summary}`,
|
|
21858
|
-
...kept.map((entry) => entry.summary)
|
|
21859
|
-
];
|
|
21860
|
-
let reviewed = yield* call("review", review_default, {
|
|
21861
|
-
base: baseline,
|
|
21862
|
-
head: checked.revision,
|
|
21863
|
-
outcome: "performance",
|
|
21864
|
-
claims
|
|
21801
|
+
const verdict = evidenceGate({
|
|
21802
|
+
...outcome === undefined ? undefined : { outcome },
|
|
21803
|
+
head: last.head,
|
|
21804
|
+
...base === undefined ? undefined : { base },
|
|
21805
|
+
...last.scope === undefined ? undefined : { review: last.scope },
|
|
21806
|
+
...reproduced === undefined ? undefined : { reproducing: reproduced.check },
|
|
21807
|
+
...implemented.documented === undefined ? undefined : { documented: implemented.documented },
|
|
21808
|
+
...implemented.rollback === undefined ? undefined : { rollback: implemented.rollback }
|
|
21865
21809
|
});
|
|
21866
|
-
for (let round = 1;round <= 2 && reviewed.findings.some(isBlocking); round++) {
|
|
21867
|
-
const blocking = reviewed.findings.filter(isBlocking);
|
|
21868
|
-
yield* agentWork(`fix:${round}`, {
|
|
21869
|
-
agent: "implementer",
|
|
21870
|
-
prompt: fixPrompt2(blocking),
|
|
21871
|
-
output: Struct({ summary: String5 })
|
|
21872
|
-
});
|
|
21873
|
-
const fixed = yield* verify(`verify:fix:${round}`, {});
|
|
21874
|
-
if (!fixed.passed) {
|
|
21875
|
-
const failing = fixed.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
21876
|
-
return yield* ending("checks_failed", `The review's fixes fail ${failing.join(", ") || "the checks"}`);
|
|
21877
|
-
}
|
|
21878
|
-
final = yield* measure(`measure:fix:${round}`, { plan, base: baseline, head: fixed.revision, ...limits });
|
|
21879
|
-
if (final.verdict !== "gain")
|
|
21880
|
-
return yield* ending("gain_gone", `The review's fixes cost the gain: ${final.reason}`, final);
|
|
21881
|
-
reviewed = yield* call(`review:${round + 1}`, review_default, {
|
|
21882
|
-
base: baseline,
|
|
21883
|
-
head: fixed.revision,
|
|
21884
|
-
outcome: "performance",
|
|
21885
|
-
claims,
|
|
21886
|
-
earlier: blocking
|
|
21887
|
-
});
|
|
21888
|
-
}
|
|
21889
21810
|
const unsettled = [
|
|
21890
|
-
...
|
|
21891
|
-
|
|
21811
|
+
...new Set([
|
|
21812
|
+
...verdict.settled ? [] : verdict.reasons,
|
|
21813
|
+
...brokenChecks(last.head).map((check) => failsOn("head", check.name, last.head.tree)),
|
|
21814
|
+
...unsettledItems(last.settled, last.decision),
|
|
21815
|
+
...last.conflicts,
|
|
21816
|
+
...criteria.flatMap((answer) => answer.status === "not_met" ? [`Criterion not met: ${answer.criterion} (${answer.detail})`] : [])
|
|
21817
|
+
])
|
|
21892
21818
|
];
|
|
21893
|
-
const
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
`)}`,
|
|
21903
|
-
`## Benchmark
|
|
21904
|
-
|
|
21905
|
-
${harness.summary}
|
|
21906
|
-
|
|
21907
|
-
\`${plan.command}\``,
|
|
21908
|
-
...unsettled.length + caveats.length === 0 ? [] : [`## Unsettled
|
|
21909
|
-
|
|
21910
|
-
${[...unsettled, ...caveats].map((entry) => `- ${entry}`).join(`
|
|
21911
|
-
`)}`]
|
|
21912
|
-
].join(`
|
|
21913
|
-
|
|
21914
|
-
`)
|
|
21819
|
+
const body = changeRequestBody({
|
|
21820
|
+
summary: implemented.summary,
|
|
21821
|
+
head: last.head,
|
|
21822
|
+
claims: last.claims,
|
|
21823
|
+
unsettled,
|
|
21824
|
+
criteria: criteria.map(answerLine),
|
|
21825
|
+
later: last.settled.open.filter((finding) => !isBlocking(finding)).map((finding) => `${finding.id}: ${finding.detail}`),
|
|
21826
|
+
rounds: last.round,
|
|
21827
|
+
stopped: last.decision.reason
|
|
21915
21828
|
});
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
|
|
21829
|
+
const named = input.changeRequestId === undefined ? undefined : input.continuing;
|
|
21830
|
+
const opened = named === undefined ? yield* changeRequest("change-request", { title: implemented.title, body }) : yield* comment("report", { changeRequestId: named.changeRequestId, body, summary: "kitman-build" }).pipe(as2({ id: named.changeRequestId, number: named.number, url: named.url }));
|
|
21831
|
+
if (named === undefined)
|
|
21832
|
+
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
21833
|
+
const approval = unsettled.length === 0 ? yield* approveChangeRequest("approve", {
|
|
21834
|
+
changeRequestId: opened.id,
|
|
21835
|
+
head: last.head.revision,
|
|
21836
|
+
summary: `Review round ${last.round} passed with nothing blocking.`,
|
|
21837
|
+
review: `review:${last.round}`
|
|
21838
|
+
}).pipe(catchTag2("StepFailed", (error) => succeed7({ approved: false, reason: error.reason }))) : undefined;
|
|
21839
|
+
const landed = input.land !== false && unsettled.length === 0 ? yield* call("land", shepherd_default, {
|
|
21840
|
+
changeRequestId: opened.id,
|
|
21841
|
+
...input.taskId === undefined ? undefined : { taskId: input.taskId },
|
|
21842
|
+
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
21843
|
+
}) : undefined;
|
|
21844
|
+
if (input.land !== false && unsettled.length > 0)
|
|
21845
|
+
yield* ask("unsettled", {
|
|
21846
|
+
prompt: `The ChangeRequest ${opened.url} is open, but these still stand: ${unsettled.join("; ")}. Build will not land it.`,
|
|
21847
|
+
options: [{ value: "Taken over", description: "A person fixes, rebuts or steers it from here." }],
|
|
21848
|
+
recommended: "Taken over",
|
|
21849
|
+
recommendationReason: "Build stopped with these unsettled, so the next step is a person\u2019s.",
|
|
21850
|
+
default: null
|
|
21851
|
+
});
|
|
21852
|
+
return {
|
|
21853
|
+
changeRequest: opened,
|
|
21854
|
+
revision: last.head.revision,
|
|
21855
|
+
tree: last.head.tree,
|
|
21856
|
+
settled: unsettled.length === 0,
|
|
21857
|
+
unsettled,
|
|
21858
|
+
rounds: last.round,
|
|
21859
|
+
stopped: last.decision.reason,
|
|
21860
|
+
criteria,
|
|
21861
|
+
...approval === undefined ? undefined : { approval },
|
|
21862
|
+
...landed === undefined ? undefined : { landed },
|
|
21863
|
+
next: { changeRequestId: opened.id }
|
|
21864
|
+
};
|
|
21865
|
+
}).pipe(tapError2(() => ignore2(writeBack("write-back:failed", { milestone: "failed" }))))
|
|
21919
21866
|
});
|
|
21920
21867
|
|
|
21921
|
-
// src/
|
|
21922
|
-
var
|
|
21923
|
-
|
|
21868
|
+
// src/errand.ts
|
|
21869
|
+
var Input4 = Struct({
|
|
21870
|
+
words: String5,
|
|
21871
|
+
taskId: optional2(String5),
|
|
21924
21872
|
repoBindingId: optional2(String5),
|
|
21925
|
-
|
|
21926
|
-
autoFix: optional2(Boolean3)
|
|
21873
|
+
branch: optional2(String5)
|
|
21927
21874
|
});
|
|
21928
|
-
var
|
|
21875
|
+
var Done3 = Struct({
|
|
21876
|
+
status: Literals(["done", "blocked", "too_big"]),
|
|
21929
21877
|
summary: String5,
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
telemetry: ArraySchema(Struct({ source: String5, reached: Boolean3, tried: optional2(String5) })),
|
|
21934
|
-
inCode: Boolean3,
|
|
21935
|
-
fix: optional2(String5)
|
|
21878
|
+
changed: Boolean3,
|
|
21879
|
+
title: optional2(String5),
|
|
21880
|
+
checks: optional2(ArraySchema(SuggestedCheck))
|
|
21936
21881
|
});
|
|
21937
|
-
var
|
|
21938
|
-
var
|
|
21939
|
-
status: Literals(["
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
conclusionArtifactId: optional2(String5),
|
|
21943
|
-
resolvedDuring: Boolean3,
|
|
21944
|
-
fix: Literals(["none", "declined", "started", "failed"]),
|
|
21945
|
-
changeRequestUrl: optional2(String5),
|
|
21946
|
-
fixRequest: optional2(FixRequest)
|
|
21882
|
+
var budget = "1 hour";
|
|
21883
|
+
var Output4 = Struct({
|
|
21884
|
+
status: Literals(["done", "blocked", "too_big", "opened", "handed_to_build", "discarded"]),
|
|
21885
|
+
summary: String5,
|
|
21886
|
+
changeRequest: optional2(ChangeRequestResult)
|
|
21947
21887
|
});
|
|
21948
|
-
var
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
|
|
21954
|
-
var isResolved = (alert) => firstText(alert, "status", "state")?.toLowerCase() === "resolved";
|
|
21955
|
-
var priorities = new Map(Object.entries({
|
|
21956
|
-
critical: "urgent",
|
|
21957
|
-
p1: "urgent",
|
|
21958
|
-
urgent: "urgent",
|
|
21959
|
-
high: "high",
|
|
21960
|
-
error: "high",
|
|
21961
|
-
p2: "high",
|
|
21962
|
-
warning: "medium",
|
|
21963
|
-
medium: "medium",
|
|
21964
|
-
p3: "medium",
|
|
21965
|
-
low: "low",
|
|
21966
|
-
info: "low",
|
|
21967
|
-
p4: "low"
|
|
21968
|
-
}));
|
|
21969
|
-
var priorityOf = (alert) => {
|
|
21970
|
-
const severity = firstText(alert, "severity", "priority", "level")?.toLowerCase();
|
|
21971
|
-
return (severity === undefined ? undefined : priorities.get(severity)) ?? "high";
|
|
21972
|
-
};
|
|
21973
|
-
var investigatePrompt = (alert) => [
|
|
21974
|
-
"Investigate this alert and conclude what happened and why.",
|
|
21975
|
-
"```json",
|
|
21976
|
-
JSON.stringify(alert, null, 2),
|
|
21977
|
-
"```",
|
|
21978
|
-
"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.",
|
|
21979
|
-
"Say whether the cause is in code Kitman can change, and propose a fix only then."
|
|
21888
|
+
var prompt2 = (words) => [
|
|
21889
|
+
`Just do this: ${words}`,
|
|
21890
|
+
implementAdaptation,
|
|
21891
|
+
"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.",
|
|
21892
|
+
"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.",
|
|
21893
|
+
"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."
|
|
21980
21894
|
].join(`
|
|
21981
21895
|
|
|
21982
21896
|
`);
|
|
21983
|
-
var
|
|
21984
|
-
|
|
21985
|
-
...resolvedDuring ? ["It resolved on its own while it was being investigated."] : [],
|
|
21986
|
-
`Cause: ${conclusion.cause} (confidence ${conclusion.confidence})`,
|
|
21987
|
-
"Sources:",
|
|
21988
|
-
...conclusion.sources.map((source) => `- ${source}`),
|
|
21989
|
-
...conclusion.telemetry.some((source) => !source.reached) ? [
|
|
21990
|
-
"Out of reach:",
|
|
21991
|
-
...conclusion.telemetry.filter((source) => !source.reached).map((source) => `- ${source.source}${source.tried === undefined ? "" : `: ${source.tried}`}`)
|
|
21992
|
-
] : [],
|
|
21993
|
-
...conclusion.fix === undefined ? [] : [`Fix: ${conclusion.fix}`]
|
|
21994
|
-
].join(`
|
|
21995
|
-
`);
|
|
21996
|
-
var firewatch_default = defineLoop({
|
|
21997
|
-
id: "firewatch",
|
|
21897
|
+
var errand_default = defineLoop({
|
|
21898
|
+
id: "errand",
|
|
21998
21899
|
version: "1.0.0",
|
|
21999
|
-
title: "
|
|
22000
|
-
input:
|
|
22001
|
-
output:
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
22005
|
-
|
|
22006
|
-
offers: [{ id: "build-fix", title: "Build the fix", loop: "build", from: "fixRequest" }],
|
|
22007
|
-
capabilities: ["agent_work", "ask", "create_tasks"],
|
|
21900
|
+
title: "Errand",
|
|
21901
|
+
input: Input4,
|
|
21902
|
+
output: Output4,
|
|
21903
|
+
agents: { implementer: "implementer" },
|
|
21904
|
+
checkout: "write",
|
|
21905
|
+
offers: [{ id: "build", title: "Build it with a full review", loop: "build" }],
|
|
21906
|
+
capabilities: ["agent_work", "verify", "change_request", "write_back", "ask"],
|
|
22008
21907
|
calls: () => [build_default],
|
|
22009
21908
|
run: (input) => gen2(function* () {
|
|
22010
|
-
const
|
|
22011
|
-
|
|
22012
|
-
|
|
22013
|
-
|
|
22014
|
-
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
|
|
22018
|
-
|
|
22019
|
-
|
|
22020
|
-
|
|
22021
|
-
|
|
22022
|
-
|
|
22023
|
-
|
|
22024
|
-
|
|
22025
|
-
|
|
22026
|
-
const conclusion = yield* agentWork("investigate", {
|
|
22027
|
-
agent: "investigator",
|
|
22028
|
-
prompt: investigatePrompt(input.alert),
|
|
22029
|
-
output: Conclusion
|
|
22030
|
-
});
|
|
22031
|
-
const resolvedDuring = (yield* alertTask("task:check", request)).resolved === true;
|
|
22032
|
-
const stored = yield* artifact("conclusion", {
|
|
22033
|
-
kind: "investigation_conclusion",
|
|
22034
|
-
title: request.title,
|
|
22035
|
-
text: conclusionText(conclusion, resolvedDuring),
|
|
22036
|
-
data: { alert: input.alert, sources: conclusion.sources, confidence: conclusion.confidence }
|
|
22037
|
-
});
|
|
22038
|
-
yield* note("summary", { text: conclusion.summary, artifactId: stored.artifactId });
|
|
22039
|
-
const concluded = {
|
|
22040
|
-
status: "concluded",
|
|
22041
|
-
...taskId,
|
|
22042
|
-
conclusionArtifactId: stored.artifactId,
|
|
22043
|
-
resolvedDuring
|
|
22044
|
-
};
|
|
22045
|
-
if (conclusion.fix === undefined || !conclusion.inCode)
|
|
22046
|
-
return { ...concluded, fix: "none" };
|
|
22047
|
-
const fixRequest = {
|
|
22048
|
-
words: `${conclusion.fix}
|
|
22049
|
-
|
|
22050
|
-
${conclusionText(conclusion, resolvedDuring)}`,
|
|
22051
|
-
outcome: "bug"
|
|
22052
|
-
};
|
|
22053
|
-
const confident = conclusion.confidence === "high" && !resolvedDuring;
|
|
22054
|
-
const answer = yield* ask("start-fix", {
|
|
22055
|
-
prompt: `${conclusion.summary}
|
|
21909
|
+
const done = yield* agentWorkOnce("errand", {
|
|
21910
|
+
agent: "implementer",
|
|
21911
|
+
skill: "implement",
|
|
21912
|
+
prompt: prompt2(input.words),
|
|
21913
|
+
output: Done3,
|
|
21914
|
+
timeout: budget
|
|
21915
|
+
}).pipe(catchIf2((error) => error._tag === "StepFailed" && error.reason.includes("time budget"), () => succeed7({
|
|
21916
|
+
status: "too_big",
|
|
21917
|
+
summary: `It took longer than an errand's ${budget}, so it is a feature: Build can do it with a full review.`,
|
|
21918
|
+
changed: false
|
|
21919
|
+
})));
|
|
21920
|
+
if (done.status !== "done" || !done.changed)
|
|
21921
|
+
return { status: done.status, summary: done.summary };
|
|
21922
|
+
const head = yield* verify("verify", { suggested: done.checks ?? [] });
|
|
21923
|
+
const answer = yield* ask("next", {
|
|
21924
|
+
prompt: `${done.summary}
|
|
22056
21925
|
|
|
22057
|
-
|
|
21926
|
+
The checks ${head.passed ? "pass" : "fail"}. What next?`,
|
|
22058
21927
|
options: [
|
|
22059
|
-
{ value: "
|
|
22060
|
-
{ value: "
|
|
21928
|
+
{ value: "Open a ChangeRequest", description: "Deliver it as it is, unreviewed." },
|
|
21929
|
+
{ value: "Hand to Build for a full review", description: "Build reviews and verifies it before delivering." },
|
|
21930
|
+
{ value: "Discard", description: "Drop the change." }
|
|
22061
21931
|
],
|
|
22062
|
-
recommended:
|
|
22063
|
-
recommendationReason:
|
|
22064
|
-
default:
|
|
21932
|
+
recommended: head.passed ? "Open a ChangeRequest" : "Hand to Build for a full review",
|
|
21933
|
+
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.",
|
|
21934
|
+
default: "Open a ChangeRequest",
|
|
22065
21935
|
timeout: "1 hour"
|
|
22066
21936
|
});
|
|
22067
|
-
if (answer === "
|
|
22068
|
-
return {
|
|
22069
|
-
|
|
22070
|
-
|
|
22071
|
-
|
|
22072
|
-
|
|
22073
|
-
|
|
21937
|
+
if (answer === "Discard")
|
|
21938
|
+
return { status: "discarded", summary: done.summary };
|
|
21939
|
+
if (answer === "Hand to Build for a full review") {
|
|
21940
|
+
const built = yield* call("build", build_default, {
|
|
21941
|
+
words: input.words,
|
|
21942
|
+
revision: head.revision,
|
|
21943
|
+
...input.taskId === undefined ? undefined : { taskId: input.taskId },
|
|
21944
|
+
...input.branch === undefined ? undefined : { branch: input.branch }
|
|
21945
|
+
});
|
|
21946
|
+
return { status: "handed_to_build", summary: done.summary, changeRequest: built.changeRequest };
|
|
21947
|
+
}
|
|
21948
|
+
const opened = yield* changeRequest("change-request", {
|
|
21949
|
+
title: done.title ?? input.words.slice(0, 120),
|
|
21950
|
+
body: [
|
|
21951
|
+
done.summary,
|
|
21952
|
+
"> **Unreviewed errand.** One agent did this with no review; ask for a Build or a Review if it needs one.",
|
|
21953
|
+
"## Evidence",
|
|
21954
|
+
evidenceTable(head)
|
|
21955
|
+
].join(`
|
|
21956
|
+
|
|
21957
|
+
`)
|
|
21958
|
+
});
|
|
21959
|
+
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
21960
|
+
return { status: "opened", summary: done.summary, changeRequest: opened };
|
|
22074
21961
|
})
|
|
22075
21962
|
});
|
|
22076
21963
|
|
|
22077
|
-
// src/
|
|
22078
|
-
var
|
|
22079
|
-
|
|
21964
|
+
// src/file-tasks.ts
|
|
21965
|
+
var FileTasksInput = Struct({
|
|
21966
|
+
tickets: PlanTickets,
|
|
21967
|
+
projectId: optional2(String5)
|
|
21968
|
+
});
|
|
21969
|
+
var Output5 = Struct({
|
|
21970
|
+
tasks: ArraySchema(Struct({ key: String5, taskId: String5 }))
|
|
21971
|
+
});
|
|
21972
|
+
var file_tasks_default = defineLoop({
|
|
21973
|
+
id: "file-tasks",
|
|
21974
|
+
version: "1.0.0",
|
|
21975
|
+
title: "File Tasks",
|
|
21976
|
+
input: FileTasksInput,
|
|
21977
|
+
output: Output5,
|
|
21978
|
+
agents: {},
|
|
21979
|
+
checkout: "none",
|
|
21980
|
+
capabilities: ["create_tasks"],
|
|
21981
|
+
run: (input) => createTasks("tasks", {
|
|
21982
|
+
tickets: input.tickets,
|
|
21983
|
+
...input.projectId === undefined ? undefined : { projectId: input.projectId }
|
|
21984
|
+
}).pipe(map8(({ tasks }) => ({ tasks })))
|
|
21985
|
+
});
|
|
21986
|
+
|
|
21987
|
+
// src/hillclimb.ts
|
|
21988
|
+
var Input5 = Struct({
|
|
21989
|
+
focus: optional2(String5),
|
|
21990
|
+
taskId: optional2(String5),
|
|
22080
21991
|
repoBindingId: optional2(String5),
|
|
22081
21992
|
branch: optional2(String5),
|
|
22082
|
-
|
|
22083
|
-
|
|
21993
|
+
cycles: optional2(Int),
|
|
21994
|
+
threshold: optional2(Finite),
|
|
21995
|
+
tolerance: optional2(Finite)
|
|
21996
|
+
});
|
|
21997
|
+
var Opportunity = Struct({
|
|
21998
|
+
title: String5,
|
|
21999
|
+
detail: String5,
|
|
22000
|
+
evidence: Literals(["measured", "telemetry", "hypothesis"]),
|
|
22001
|
+
external: optional2(Boolean3)
|
|
22002
|
+
});
|
|
22003
|
+
var Discovery = Struct({
|
|
22004
|
+
stack: String5,
|
|
22005
|
+
journeys: ArraySchema(Struct({ name: String5, objective: String5 })),
|
|
22006
|
+
telemetry: ArraySchema(Struct({ source: String5, status: Literals(["used", "unavailable"]), why: String5 })),
|
|
22007
|
+
opportunities: ArraySchema(Opportunity)
|
|
22084
22008
|
});
|
|
22085
|
-
var
|
|
22086
|
-
|
|
22087
|
-
|
|
22088
|
-
tickets: PlanTickets,
|
|
22089
|
-
outOfScope: ArraySchema(String5),
|
|
22090
|
-
afterMerge: ArraySchema(String5),
|
|
22091
|
-
docs: optional2(Struct({ title: String5, summary: String5 }))
|
|
22009
|
+
var Harness = Struct({
|
|
22010
|
+
summary: String5,
|
|
22011
|
+
plan: MeasurePlan
|
|
22092
22012
|
});
|
|
22093
|
-
var
|
|
22094
|
-
|
|
22013
|
+
var Change = Struct({
|
|
22014
|
+
status: Literals(["changed", "exhausted", "unsafe"]),
|
|
22015
|
+
summary: String5
|
|
22095
22016
|
});
|
|
22096
|
-
var
|
|
22097
|
-
var
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22017
|
+
var Kept = Struct({ summary: String5, verdict: String5, reason: String5 });
|
|
22018
|
+
var Output6 = Struct({
|
|
22019
|
+
status: Literals(["improved", "no_gain", "gain_gone", "checks_failed", "too_noisy"]),
|
|
22020
|
+
reason: String5,
|
|
22021
|
+
kept: ArraySchema(Kept),
|
|
22022
|
+
cycles: Int,
|
|
22023
|
+
reportArtifactId: String5,
|
|
22103
22024
|
changeRequest: optional2(ChangeRequestResult),
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
var Output7 = Struct({
|
|
22107
|
-
title: String5,
|
|
22108
|
-
outOfScope: ArraySchema(String5),
|
|
22109
|
-
afterMerge: ArraySchema(String5),
|
|
22110
|
-
rejected: ArraySchema(Rejected),
|
|
22111
|
-
stopped: optional2(String5),
|
|
22112
|
-
implemented: Boolean3,
|
|
22113
|
-
tickets: ArraySchema(TicketResult),
|
|
22114
|
-
deliveries: ArraySchema(ChangeRequestResult),
|
|
22115
|
-
tasks: optional2(CreateTasksResult.fields.tasks),
|
|
22116
|
-
docs: optional2(ChangeRequestResult)
|
|
22025
|
+
external: ArraySchema(String5),
|
|
22026
|
+
unsettled: optional2(ArraySchema(String5))
|
|
22117
22027
|
});
|
|
22118
|
-
var
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
"Implement now": "Builds the tickets in dependency order and delivers them.",
|
|
22123
|
-
"File as Tasks": "Makes a Task of each ticket, to build later.",
|
|
22124
|
-
"Second opinion": "Another Agent reviews the plan.",
|
|
22125
|
-
Refine: "You say what to change and the planner rewrites it.",
|
|
22126
|
-
Finish: "Ends the Run with the plan as it is."
|
|
22127
|
-
};
|
|
22128
|
-
var onFailure = ["Retry with a steer", "Skip it and its dependents", "Stop here"];
|
|
22129
|
-
var ticketRules = [
|
|
22130
|
-
"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.",
|
|
22131
|
-
"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."
|
|
22028
|
+
var discoverPrompt = (focus) => [
|
|
22029
|
+
`Find where this repository is slow${focus === undefined ? ", for the journeys that matter most" : `: ${focus}`}. Change nothing.`,
|
|
22030
|
+
"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.",
|
|
22031
|
+
"Rank the opportunities, most promising first, each marked measured, telemetry or hypothesis. Mark one that only another repository can make as external."
|
|
22132
22032
|
].join(`
|
|
22133
22033
|
|
|
22134
22034
|
`);
|
|
22135
|
-
var
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
"
|
|
22139
|
-
|
|
22140
|
-
"List in outOfScope what the plan deliberately leaves out, one item each; each holds the Builds of its tickets.",
|
|
22141
|
-
"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."
|
|
22035
|
+
var harnessPrompt = (discovery) => [
|
|
22036
|
+
"Commit a local benchmark for these journeys, and change no product code:",
|
|
22037
|
+
...discovery.journeys.map((journey) => `- ${journey.name}: ${journey.objective}`),
|
|
22038
|
+
"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.",
|
|
22039
|
+
"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."
|
|
22142
22040
|
].join(`
|
|
22143
22041
|
|
|
22144
22042
|
`);
|
|
22145
|
-
var
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
...
|
|
22149
|
-
`)],
|
|
22150
|
-
|
|
22151
|
-
`)],
|
|
22152
|
-
"## Tickets",
|
|
22153
|
-
...plan.tickets.map(ticketText)
|
|
22043
|
+
var changePrompt = (discovery, plan, tried, cycle) => [
|
|
22044
|
+
`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.`,
|
|
22045
|
+
"Opportunities, most promising first:",
|
|
22046
|
+
...discovery.opportunities.flatMap((entry) => entry.external === true ? [] : [`- ${entry.title} (${entry.evidence}): ${entry.detail}`]),
|
|
22047
|
+
...tried.length === 0 ? [] : ["Already tried:", ...tried.map((entry) => `- ${entry}`)],
|
|
22048
|
+
"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."
|
|
22154
22049
|
].join(`
|
|
22155
22050
|
|
|
22156
22051
|
`);
|
|
22157
|
-
var
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
...
|
|
22161
|
-
...ticket.blockedBy.length === 0 ? [] : [`Blocked by: ${ticket.blockedBy.join(", ")}`]
|
|
22052
|
+
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.`;
|
|
22053
|
+
var fixPrompt2 = (findings) => [
|
|
22054
|
+
"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.",
|
|
22055
|
+
...findings.map((finding) => `- ${finding.id}: ${finding.detail}`)
|
|
22162
22056
|
].join(`
|
|
22163
22057
|
|
|
22164
22058
|
`);
|
|
22165
|
-
var
|
|
22166
|
-
|
|
22167
|
-
|
|
22059
|
+
var percent = (fraction) => `${(fraction * 100).toFixed(1)}%`;
|
|
22060
|
+
var numbers = (measured) => [
|
|
22061
|
+
"| Metric | Role | Base median | Head median | Change | Noise |",
|
|
22062
|
+
"|---|---|---|---|---|---|",
|
|
22063
|
+
...measured.metrics.map((metric) => `| ${metric.name} | ${metric.role} | ${metric.base.median} | ${metric.head.median} | ${percent(metric.change)} | ${percent(metric.noise)} |`)
|
|
22168
22064
|
].join(`
|
|
22169
|
-
|
|
22170
22065
|
`);
|
|
22171
|
-
var
|
|
22066
|
+
var report = (input) => [
|
|
22067
|
+
`# Hillclimb findings
|
|
22172
22068
|
|
|
22173
|
-
|
|
22174
|
-
|
|
22175
|
-
"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.",
|
|
22176
|
-
planText(plan)
|
|
22177
|
-
].join(`
|
|
22069
|
+
${input.reason}`,
|
|
22070
|
+
`## Stack
|
|
22178
22071
|
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
var guarded = (plan, check) => {
|
|
22182
|
-
const met = (ticket, criterion) => check.alreadyMet.some((entry) => entry.key === ticket.key && entry.criterion === criterion);
|
|
22183
|
-
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" }] : []);
|
|
22184
|
-
const gone = new Set(rejected.map((entry) => entry.key));
|
|
22185
|
-
const tickets = plan.tickets.flatMap((ticket) => gone.has(ticket.key) ? [] : [
|
|
22186
|
-
{
|
|
22187
|
-
...ticket,
|
|
22188
|
-
acceptanceCriteria: ticket.acceptanceCriteria.map((criterion) => met(ticket, criterion) ? `${criterion}${guard2}` : criterion),
|
|
22189
|
-
blockedBy: ticket.blockedBy.filter((key) => !gone.has(key))
|
|
22190
|
-
}
|
|
22191
|
-
]);
|
|
22192
|
-
return { plan: { ...plan, tickets }, rejected };
|
|
22193
|
-
};
|
|
22194
|
-
var checked = (plan, revision) => map8(agentWork(`criteria-check:${revision}`, { agent: "reviewer", prompt: criteriaPrompt2(plan), output: CriteriaCheck }), (check) => guarded(plan, check));
|
|
22195
|
-
var storePlan = (plan, suffix) => gen2(function* () {
|
|
22196
|
-
yield* artifact(`spec${suffix}`, {
|
|
22197
|
-
kind: "plan_spec",
|
|
22198
|
-
title: plan.title,
|
|
22199
|
-
text: plan.spec,
|
|
22200
|
-
data: { outOfScope: [...plan.outOfScope] }
|
|
22201
|
-
});
|
|
22202
|
-
yield* artifact(`tickets${suffix}`, {
|
|
22203
|
-
kind: "plan_tickets",
|
|
22204
|
-
title: `Tickets for ${plan.title}`,
|
|
22205
|
-
text: plan.tickets.map(ticketText).join(`
|
|
22072
|
+
${input.discovery.stack}`,
|
|
22073
|
+
`## Telemetry
|
|
22206
22074
|
|
|
22207
|
-
`)
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
acceptanceCriteria: [...ticket.acceptanceCriteria],
|
|
22211
|
-
blockedBy: [...ticket.blockedBy],
|
|
22212
|
-
...repoBindingId === undefined ? undefined : { repoBindingId },
|
|
22213
|
-
...outcome === undefined ? undefined : { outcome }
|
|
22214
|
-
}))
|
|
22215
|
-
});
|
|
22216
|
-
});
|
|
22217
|
-
var changelog = (before, after, words) => {
|
|
22218
|
-
const was = new Map(before.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
22219
|
-
const now = new Map(after.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
22220
|
-
const lines = [
|
|
22221
|
-
...after.tickets.filter((ticket) => !was.has(ticket.key)).map((ticket) => `- Added ${ticket.key}: ${ticket.title}`),
|
|
22222
|
-
...before.tickets.filter((ticket) => !now.has(ticket.key)).map((ticket) => `- Removed ${ticket.key}: ${ticket.title}`),
|
|
22223
|
-
...after.tickets.filter((ticket) => was.has(ticket.key) && was.get(ticket.key) !== now.get(ticket.key)).map((ticket) => `- Changed ${ticket.key}: ${ticket.title}`),
|
|
22224
|
-
...before.spec === after.spec ? [] : ["- Changed the spec"],
|
|
22225
|
-
...before.title === after.title ? [] : [`- Renamed the plan from "${before.title}"`]
|
|
22226
|
-
];
|
|
22227
|
-
return [`Asked: ${words}`, ...lines.length === 0 ? ["Nothing changed."] : lines].join(`
|
|
22228
|
-
`);
|
|
22229
|
-
};
|
|
22230
|
-
var buildWords = (ticket) => [
|
|
22231
|
-
ticket.title,
|
|
22232
|
-
ticket.body,
|
|
22233
|
-
...ticket.acceptanceCriteria.length === 0 ? [] : ["Acceptance criteria:", ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)]
|
|
22234
|
-
].join(`
|
|
22075
|
+
${input.discovery.telemetry.map((entry) => `- ${entry.source}: ${entry.status}, ${entry.why}`).join(`
|
|
22076
|
+
`) || "- None reached"}`,
|
|
22077
|
+
`## Benchmark
|
|
22235
22078
|
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
key: ticket.key,
|
|
22239
|
-
title: ticket.title,
|
|
22240
|
-
...result
|
|
22241
|
-
});
|
|
22242
|
-
var buildTicket = (ticket, start, taskId, retry) => gen2(function* () {
|
|
22243
|
-
let head = start;
|
|
22244
|
-
const name = retry === undefined ? ticket.key : `${ticket.key}:${retry.label}`;
|
|
22245
|
-
for (let attempt = 1;; attempt++) {
|
|
22246
|
-
const suffix = attempt === 1 ? "" : `:${attempt}`;
|
|
22247
|
-
const words = retry === undefined ? taskId === undefined ? { words: buildWords(ticket) } : undefined : { words: `${buildWords(ticket)}
|
|
22079
|
+
${input.harness.summary}`,
|
|
22080
|
+
`## Tried
|
|
22248
22081
|
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
|
|
22252
|
-
|
|
22253
|
-
|
|
22254
|
-
|
|
22255
|
-
|
|
22256
|
-
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
|
|
22262
|
-
|
|
22263
|
-
|
|
22264
|
-
|
|
22265
|
-
return settled(ticket, { status: "merged", ...delivered });
|
|
22266
|
-
const choice = yield* ask(`conflict:${name}${suffix}`, {
|
|
22267
|
-
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?`,
|
|
22268
|
-
options: [
|
|
22269
|
-
{
|
|
22270
|
-
value: "Rebuild on the integration branch",
|
|
22271
|
-
description: "Builds the ticket again from the branch as it is now."
|
|
22272
|
-
},
|
|
22273
|
-
{ value: "Skip", description: "Leaves it and the tickets it blocks undelivered." }
|
|
22274
|
-
],
|
|
22275
|
-
recommended: "Rebuild on the integration branch",
|
|
22276
|
-
recommendationReason: "A rebuild on the current branch usually resolves a conflict, and skipping also drops every ticket it blocks.",
|
|
22277
|
-
default: "Skip",
|
|
22278
|
-
timeout: "1 hour"
|
|
22279
|
-
});
|
|
22280
|
-
if (choice === "Skip")
|
|
22281
|
-
return settled(ticket, { status: "failed", ...delivered, reason: `It conflicted with ${head.branch}` });
|
|
22282
|
-
head = yield* integrationBranch(`rebuild-from:${name}:${attempt + 1}`, {
|
|
22283
|
-
repoBindingId: head.repoBindingId
|
|
22284
|
-
});
|
|
22285
|
-
}
|
|
22286
|
-
});
|
|
22287
|
-
var deliveryBody = (plan, results) => {
|
|
22288
|
-
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}`;
|
|
22289
|
-
const section = (title, status) => {
|
|
22290
|
-
const lines = results.flatMap((result) => result.status === status ? [line(result)] : []);
|
|
22291
|
-
return lines.length === 0 ? [] : [`## ${title}`, lines.join(`
|
|
22292
|
-
`)];
|
|
22293
|
-
};
|
|
22294
|
-
return [
|
|
22295
|
-
`Delivers the plan "${plan.title}".`,
|
|
22296
|
-
...section("Merged", "merged"),
|
|
22297
|
-
...section("Failed", "failed"),
|
|
22298
|
-
...section("Skipped", "skipped")
|
|
22299
|
-
].join(`
|
|
22082
|
+
${input.tried.map((entry) => `- ${entry}`).join(`
|
|
22083
|
+
`) || "- Nothing"}`,
|
|
22084
|
+
...input.kept.length === 0 ? [] : [`## Kept
|
|
22085
|
+
|
|
22086
|
+
${input.kept.map((entry) => `- ${entry.summary}: ${entry.reason}`).join(`
|
|
22087
|
+
`)}`],
|
|
22088
|
+
...input.final === undefined ? [] : [`## Final measurement
|
|
22089
|
+
|
|
22090
|
+
${input.final.reason}
|
|
22091
|
+
|
|
22092
|
+
${numbers(input.final)}`],
|
|
22093
|
+
`## Opportunities
|
|
22094
|
+
|
|
22095
|
+
${input.discovery.opportunities.map((entry) => `- ${entry.title} (${entry.evidence}${entry.external === true ? ", external" : ""}): ${entry.detail}`).join(`
|
|
22096
|
+
`)}`
|
|
22097
|
+
].join(`
|
|
22300
22098
|
|
|
22301
22099
|
`);
|
|
22100
|
+
var steadier = (plan) => {
|
|
22101
|
+
const counter = plan.metrics.find((metric) => metric.lowVariance === true && metric.role !== "primary");
|
|
22102
|
+
return counter === undefined ? undefined : {
|
|
22103
|
+
...plan,
|
|
22104
|
+
metrics: plan.metrics.map((metric) => ({
|
|
22105
|
+
...metric,
|
|
22106
|
+
role: metric.name === counter.name ? "primary" : "guardrail"
|
|
22107
|
+
}))
|
|
22108
|
+
};
|
|
22302
22109
|
};
|
|
22303
|
-
var
|
|
22304
|
-
|
|
22305
|
-
|
|
22306
|
-
|
|
22307
|
-
|
|
22308
|
-
|
|
22309
|
-
|
|
22310
|
-
|
|
22311
|
-
|
|
22312
|
-
|
|
22313
|
-
|
|
22314
|
-
|
|
22315
|
-
|
|
22316
|
-
|
|
22317
|
-
|
|
22318
|
-
|
|
22319
|
-
|
|
22320
|
-
|
|
22321
|
-
|
|
22110
|
+
var hillclimb_default = defineLoop({
|
|
22111
|
+
id: "hillclimb",
|
|
22112
|
+
version: "1.3.0",
|
|
22113
|
+
title: "Hillclimb",
|
|
22114
|
+
outcome: "performance",
|
|
22115
|
+
input: Input5,
|
|
22116
|
+
output: Output6,
|
|
22117
|
+
agents: { investigator: "climber", implementer: "implementer" },
|
|
22118
|
+
checkout: "write",
|
|
22119
|
+
capabilities: ["agent_work", "verify", "measure", "change_request", "write_back", "ask", "create_tasks"],
|
|
22120
|
+
calls: () => [review_default],
|
|
22121
|
+
offers: [{ id: "retry", title: "Climb again", loop: "hillclimb" }],
|
|
22122
|
+
run: (input) => gen2(function* () {
|
|
22123
|
+
const limits = {
|
|
22124
|
+
...input.threshold === undefined ? undefined : { threshold: input.threshold },
|
|
22125
|
+
...input.tolerance === undefined ? undefined : { tolerance: input.tolerance }
|
|
22126
|
+
};
|
|
22127
|
+
const discovery = yield* agentWork("discover", {
|
|
22128
|
+
agent: "investigator",
|
|
22129
|
+
prompt: discoverPrompt(input.focus),
|
|
22130
|
+
output: Discovery
|
|
22131
|
+
});
|
|
22132
|
+
const external = discovery.opportunities.flatMap((entry) => entry.external === true ? [entry.title] : []);
|
|
22133
|
+
yield* syncBase("sync:start", {
|
|
22134
|
+
agent: "implementer",
|
|
22135
|
+
...input.branch === undefined ? undefined : { target: input.branch }
|
|
22136
|
+
});
|
|
22137
|
+
const harness = yield* agentWorkOnce("harness", {
|
|
22138
|
+
agent: "implementer",
|
|
22139
|
+
prompt: harnessPrompt(discovery),
|
|
22140
|
+
output: Harness
|
|
22141
|
+
});
|
|
22142
|
+
const baseline = (yield* verify("verify:harness", {})).revision;
|
|
22143
|
+
let plan = harness.plan;
|
|
22144
|
+
const calibrated = yield* measure("calibrate", { plan, base: baseline, head: baseline, ...limits });
|
|
22145
|
+
const kept = [];
|
|
22146
|
+
const tried = [];
|
|
22147
|
+
let cycles = 0;
|
|
22148
|
+
const ending = (status, reason, final) => gen2(function* () {
|
|
22149
|
+
const stored = yield* artifact("report", {
|
|
22150
|
+
kind: "hillclimb-report",
|
|
22151
|
+
title: "Hillclimb findings",
|
|
22152
|
+
text: report({ discovery, harness, reason, kept, tried, final })
|
|
22153
|
+
});
|
|
22154
|
+
return { status, reason, kept, cycles, reportArtifactId: stored.artifactId, external };
|
|
22155
|
+
});
|
|
22156
|
+
if (calibrated.verdict === "too_noisy") {
|
|
22157
|
+
const switched = steadier(plan);
|
|
22158
|
+
if (switched === undefined)
|
|
22159
|
+
return yield* ending("too_noisy", `${calibrated.reason}. ${calibrated.suggestion ?? ""}`.trim());
|
|
22160
|
+
plan = switched;
|
|
22322
22161
|
}
|
|
22323
|
-
|
|
22324
|
-
|
|
22325
|
-
for (
|
|
22326
|
-
|
|
22327
|
-
|
|
22328
|
-
|
|
22329
|
-
|
|
22330
|
-
|
|
22331
|
-
|
|
22332
|
-
|
|
22333
|
-
|
|
22334
|
-
|
|
22335
|
-
|
|
22336
|
-
|
|
22337
|
-
|
|
22338
|
-
|
|
22339
|
-
|
|
22340
|
-
|
|
22341
|
-
|
|
22342
|
-
|
|
22343
|
-
|
|
22344
|
-
|
|
22345
|
-
|
|
22346
|
-
|
|
22347
|
-
|
|
22348
|
-
|
|
22349
|
-
|
|
22350
|
-
|
|
22351
|
-
|
|
22352
|
-
|
|
22353
|
-
|
|
22354
|
-
|
|
22355
|
-
}
|
|
22356
|
-
|
|
22357
|
-
|
|
22162
|
+
let start = baseline;
|
|
22163
|
+
let unkept = 0;
|
|
22164
|
+
for (let cycle = 1;cycle <= (input.cycles ?? 5) && unkept < 2; cycle++) {
|
|
22165
|
+
const change = yield* agentWorkOnce(`change:${cycle}`, {
|
|
22166
|
+
agent: "implementer",
|
|
22167
|
+
prompt: changePrompt(discovery, plan, tried, cycle),
|
|
22168
|
+
output: Change
|
|
22169
|
+
});
|
|
22170
|
+
if (change.status !== "changed") {
|
|
22171
|
+
tried.push(`Cycle ${cycle}: ${change.status}, ${change.summary}`);
|
|
22172
|
+
break;
|
|
22173
|
+
}
|
|
22174
|
+
cycles = cycle;
|
|
22175
|
+
const head = yield* verify(`verify:${cycle}`, {});
|
|
22176
|
+
const measured = head.passed ? yield* measure(`measure:${cycle}`, { plan, base: start, head: head.revision, ...limits }) : undefined;
|
|
22177
|
+
if (measured?.verdict === "gain") {
|
|
22178
|
+
kept.push({ summary: change.summary, verdict: measured.verdict, reason: measured.reason });
|
|
22179
|
+
tried.push(`Cycle ${cycle}, kept: ${change.summary} (${measured.reason})`);
|
|
22180
|
+
start = head.revision;
|
|
22181
|
+
unkept = 0;
|
|
22182
|
+
continue;
|
|
22183
|
+
}
|
|
22184
|
+
tried.push(`Cycle ${cycle}, reverted: ${change.summary} (${measured === undefined ? "its checks failed" : measured.reason})`);
|
|
22185
|
+
yield* agentWork(`revert:${cycle}`, {
|
|
22186
|
+
agent: "implementer",
|
|
22187
|
+
prompt: revertPrompt(start, cycle),
|
|
22188
|
+
output: Struct({ reverted: Boolean3 })
|
|
22189
|
+
});
|
|
22190
|
+
unkept++;
|
|
22191
|
+
}
|
|
22192
|
+
if (external.length > 0) {
|
|
22193
|
+
const file = yield* ask("external", {
|
|
22194
|
+
prompt: `Only other repositories can make these improvements: ${external.join("; ")}. File them as Tasks?`,
|
|
22195
|
+
options: ["File as Tasks", "Leave them"],
|
|
22196
|
+
recommended: "File as Tasks",
|
|
22197
|
+
recommendationReason: "Filed as Tasks, the improvements wait in the repositories that can make them instead of being lost.",
|
|
22198
|
+
default: "Leave them",
|
|
22199
|
+
timeout: "1 hour"
|
|
22200
|
+
});
|
|
22201
|
+
if (file === "File as Tasks")
|
|
22202
|
+
yield* createTasks("external-tasks", {
|
|
22203
|
+
tickets: discovery.opportunities.flatMap((entry, index) => entry.external === true ? [
|
|
22204
|
+
{
|
|
22205
|
+
key: `external-${index + 1}`,
|
|
22206
|
+
title: entry.title,
|
|
22207
|
+
body: entry.detail,
|
|
22208
|
+
acceptanceCriteria: [],
|
|
22209
|
+
blockedBy: [],
|
|
22210
|
+
outcome: "performance"
|
|
22211
|
+
}
|
|
22212
|
+
] : [])
|
|
22358
22213
|
});
|
|
22359
|
-
|
|
22214
|
+
}
|
|
22215
|
+
if (kept.length === 0)
|
|
22216
|
+
return yield* ending("no_gain", "No change proved faster than the noise allows");
|
|
22217
|
+
const synced = yield* syncBase("sync:final", {
|
|
22218
|
+
agent: "implementer",
|
|
22219
|
+
...input.branch === undefined ? undefined : { target: input.branch }
|
|
22220
|
+
});
|
|
22221
|
+
const checked = yield* verify("verify:final", {});
|
|
22222
|
+
if (!checked.passed) {
|
|
22223
|
+
const failing = checked.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
22224
|
+
return yield* ending("checks_failed", `The final tree fails ${failing.join(", ") || "its checks"}`);
|
|
22225
|
+
}
|
|
22226
|
+
let final = yield* measure("measure:final", { plan, base: baseline, head: checked.revision, ...limits });
|
|
22227
|
+
if (final.verdict !== "gain")
|
|
22228
|
+
return yield* ending("gain_gone", `The final tree no longer proves the gain: ${final.reason}`, final);
|
|
22229
|
+
const claims = [
|
|
22230
|
+
"Behaviour is preserved: every kept change leaves what the code does exactly as it was.",
|
|
22231
|
+
`The measurement is valid: ${harness.summary}`,
|
|
22232
|
+
...kept.map((entry) => entry.summary)
|
|
22233
|
+
];
|
|
22234
|
+
let reviewed = yield* call("review", review_default, {
|
|
22235
|
+
base: baseline,
|
|
22236
|
+
head: checked.revision,
|
|
22237
|
+
outcome: "performance",
|
|
22238
|
+
claims
|
|
22239
|
+
});
|
|
22240
|
+
for (let round = 1;round <= 2 && reviewed.findings.some(isBlocking); round++) {
|
|
22241
|
+
const blocking = reviewed.findings.filter(isBlocking);
|
|
22242
|
+
yield* agentWork(`fix:${round}`, {
|
|
22243
|
+
agent: "implementer",
|
|
22244
|
+
prompt: fixPrompt2(blocking),
|
|
22245
|
+
output: Struct({ summary: String5 })
|
|
22246
|
+
});
|
|
22247
|
+
const fixed = yield* verify(`verify:fix:${round}`, {});
|
|
22248
|
+
if (!fixed.passed) {
|
|
22249
|
+
const failing = fixed.checks.filter((check) => check.status === "failed").map((check) => check.name);
|
|
22250
|
+
return yield* ending("checks_failed", `The review's fixes fail ${failing.join(", ") || "the checks"}`);
|
|
22360
22251
|
}
|
|
22361
|
-
|
|
22252
|
+
final = yield* measure(`measure:fix:${round}`, { plan, base: baseline, head: fixed.revision, ...limits });
|
|
22253
|
+
if (final.verdict !== "gain")
|
|
22254
|
+
return yield* ending("gain_gone", `The review's fixes cost the gain: ${final.reason}`, final);
|
|
22255
|
+
reviewed = yield* call(`review:${round + 1}`, review_default, {
|
|
22256
|
+
base: baseline,
|
|
22257
|
+
head: fixed.revision,
|
|
22258
|
+
outcome: "performance",
|
|
22259
|
+
claims,
|
|
22260
|
+
earlier: blocking
|
|
22261
|
+
});
|
|
22362
22262
|
}
|
|
22363
|
-
|
|
22364
|
-
|
|
22365
|
-
|
|
22366
|
-
|
|
22367
|
-
|
|
22368
|
-
|
|
22369
|
-
|
|
22370
|
-
|
|
22371
|
-
|
|
22372
|
-
|
|
22373
|
-
|
|
22263
|
+
const unsettled = [
|
|
22264
|
+
...[...reviewed.findings, ...scopeFindings(reviewed.scope)].flatMap((finding) => isBlocking(finding) ? [`${finding.id}: ${finding.detail}`] : []),
|
|
22265
|
+
...synced.status === "conflict" ? [`Merging the base in conflicts on ${synced.files.join(", ")}`] : []
|
|
22266
|
+
];
|
|
22267
|
+
const caveats = synced.status === "merged" ? ["The base moved during the climb; the final measurement includes its changes too."] : [];
|
|
22268
|
+
const opened = yield* changeRequest("change-request", {
|
|
22269
|
+
title: `Faster: ${kept.map((entry) => entry.summary).join("; ")}`.slice(0, 240),
|
|
22270
|
+
body: [
|
|
22271
|
+
`${final.reason}, measured by Kitman against the benchmark on the base.`,
|
|
22272
|
+
numbers(final),
|
|
22273
|
+
`## Kept
|
|
22274
|
+
|
|
22275
|
+
${kept.map((entry) => `- ${entry.summary}: ${entry.reason}`).join(`
|
|
22276
|
+
`)}`,
|
|
22277
|
+
`## Benchmark
|
|
22278
|
+
|
|
22279
|
+
${harness.summary}
|
|
22280
|
+
|
|
22281
|
+
\`${plan.command}\``,
|
|
22282
|
+
...unsettled.length + caveats.length === 0 ? [] : [`## Unsettled
|
|
22283
|
+
|
|
22284
|
+
${[...unsettled, ...caveats].map((entry) => `- ${entry}`).join(`
|
|
22285
|
+
`)}`]
|
|
22286
|
+
].join(`
|
|
22287
|
+
|
|
22288
|
+
`)
|
|
22289
|
+
});
|
|
22290
|
+
yield* writeBack("write-back", { milestone: "change_request_opened", changeRequestUrl: opened.url });
|
|
22291
|
+
return { ...yield* ending("improved", final.reason, final), changeRequest: opened, unsettled };
|
|
22292
|
+
})
|
|
22374
22293
|
});
|
|
22375
|
-
|
|
22376
|
-
|
|
22377
|
-
|
|
22378
|
-
|
|
22294
|
+
|
|
22295
|
+
// src/firewatch.ts
|
|
22296
|
+
var Input6 = Struct({
|
|
22297
|
+
alert: Record(String5, Json2),
|
|
22298
|
+
repoBindingId: optional2(String5),
|
|
22299
|
+
windowHours: optional2(Int.check(isGreaterThanOrEqualTo2(0), isLessThanOrEqualTo3(720))),
|
|
22300
|
+
autoFix: optional2(Boolean3)
|
|
22301
|
+
});
|
|
22302
|
+
var Conclusion = Struct({
|
|
22303
|
+
summary: String5,
|
|
22304
|
+
cause: String5,
|
|
22305
|
+
confidence: Literals(["high", "medium", "low"]),
|
|
22306
|
+
sources: NonEmptyArray(String5),
|
|
22307
|
+
telemetry: ArraySchema(Struct({ source: String5, reached: Boolean3, tried: optional2(String5) })),
|
|
22308
|
+
inCode: Boolean3,
|
|
22309
|
+
fix: optional2(String5)
|
|
22310
|
+
});
|
|
22311
|
+
var FixRequest = Struct({ words: String5, outcome: Literal2("bug") });
|
|
22312
|
+
var Output7 = Struct({
|
|
22313
|
+
status: Literals(["folded", "resolved", "concluded"]),
|
|
22314
|
+
taskId: optional2(String5),
|
|
22315
|
+
joinedRunId: optional2(String5),
|
|
22316
|
+
conclusionArtifactId: optional2(String5),
|
|
22317
|
+
resolvedDuring: Boolean3,
|
|
22318
|
+
fix: Literals(["none", "declined", "started", "failed"]),
|
|
22319
|
+
changeRequestUrl: optional2(String5),
|
|
22320
|
+
fixRequest: optional2(FixRequest)
|
|
22321
|
+
});
|
|
22322
|
+
var firstText = (alert, ...keys) => keys.flatMap((key) => {
|
|
22323
|
+
const value = alert[key];
|
|
22324
|
+
return isString(value) && value.trim() !== "" ? [value] : [];
|
|
22325
|
+
})[0];
|
|
22326
|
+
var alertTitle = (alert) => firstText(alert, "title", "name", "summary") ?? "Alert";
|
|
22327
|
+
var fingerprintOf = (alert) => firstText(alert, "fingerprint", "groupKey") ?? alertTitle(alert);
|
|
22328
|
+
var isResolved = (alert) => firstText(alert, "status", "state")?.toLowerCase() === "resolved";
|
|
22329
|
+
var priorities = new Map(Object.entries({
|
|
22330
|
+
critical: "urgent",
|
|
22331
|
+
p1: "urgent",
|
|
22332
|
+
urgent: "urgent",
|
|
22333
|
+
high: "high",
|
|
22334
|
+
error: "high",
|
|
22335
|
+
p2: "high",
|
|
22336
|
+
warning: "medium",
|
|
22337
|
+
medium: "medium",
|
|
22338
|
+
p3: "medium",
|
|
22339
|
+
low: "low",
|
|
22340
|
+
info: "low",
|
|
22341
|
+
p4: "low"
|
|
22342
|
+
}));
|
|
22343
|
+
var priorityOf = (alert) => {
|
|
22344
|
+
const severity = firstText(alert, "severity", "priority", "level")?.toLowerCase();
|
|
22345
|
+
return (severity === undefined ? undefined : priorities.get(severity)) ?? "high";
|
|
22346
|
+
};
|
|
22347
|
+
var investigatePrompt = (alert) => [
|
|
22348
|
+
"Investigate this alert and conclude what happened and why.",
|
|
22349
|
+
"```json",
|
|
22350
|
+
JSON.stringify(alert, null, 2),
|
|
22351
|
+
"```",
|
|
22352
|
+
"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.",
|
|
22353
|
+
"Say whether the cause is in code Kitman can change, and propose a fix only then."
|
|
22354
|
+
].join(`
|
|
22355
|
+
|
|
22356
|
+
`);
|
|
22357
|
+
var conclusionText = (conclusion, resolvedDuring) => [
|
|
22358
|
+
conclusion.summary,
|
|
22359
|
+
...resolvedDuring ? ["It resolved on its own while it was being investigated."] : [],
|
|
22360
|
+
`Cause: ${conclusion.cause} (confidence ${conclusion.confidence})`,
|
|
22361
|
+
"Sources:",
|
|
22362
|
+
...conclusion.sources.map((source) => `- ${source}`),
|
|
22363
|
+
...conclusion.telemetry.some((source) => !source.reached) ? [
|
|
22364
|
+
"Out of reach:",
|
|
22365
|
+
...conclusion.telemetry.filter((source) => !source.reached).map((source) => `- ${source.source}${source.tried === undefined ? "" : `: ${source.tried}`}`)
|
|
22366
|
+
] : [],
|
|
22367
|
+
...conclusion.fix === undefined ? [] : [`Fix: ${conclusion.fix}`]
|
|
22368
|
+
].join(`
|
|
22369
|
+
`);
|
|
22370
|
+
var firewatch_default = defineLoop({
|
|
22371
|
+
id: "firewatch",
|
|
22372
|
+
version: "1.0.0",
|
|
22373
|
+
title: "Firewatch",
|
|
22379
22374
|
input: Input6,
|
|
22380
22375
|
output: Output7,
|
|
22381
|
-
|
|
22382
|
-
|
|
22383
|
-
|
|
22384
|
-
|
|
22385
|
-
|
|
22386
|
-
|
|
22387
|
-
"verify",
|
|
22388
|
-
"change_request",
|
|
22389
|
-
"write_back",
|
|
22390
|
-
"create_tasks",
|
|
22391
|
-
"merge_change_request",
|
|
22392
|
-
"read_change_request",
|
|
22393
|
-
"post_comment"
|
|
22394
|
-
],
|
|
22376
|
+
outcome: "investigation",
|
|
22377
|
+
agents: { investigator: "investigator" },
|
|
22378
|
+
checkout: "read",
|
|
22379
|
+
when: [{ event: "monitoring.alert", input: { alert: "alert" } }],
|
|
22380
|
+
offers: [{ id: "build-fix", title: "Build the fix", loop: "build", from: "fixRequest" }],
|
|
22381
|
+
capabilities: ["agent_work", "ask", "create_tasks"],
|
|
22395
22382
|
calls: () => [build_default],
|
|
22396
22383
|
run: (input) => gen2(function* () {
|
|
22397
|
-
const
|
|
22398
|
-
|
|
22399
|
-
|
|
22400
|
-
|
|
22401
|
-
|
|
22384
|
+
const fingerprint = fingerprintOf(input.alert);
|
|
22385
|
+
const resolved = isResolved(input.alert);
|
|
22386
|
+
const windowHours = input.windowHours ?? 6;
|
|
22387
|
+
const request = { fingerprint, title: alertTitle(input.alert), windowHours, priority: priorityOf(input.alert) };
|
|
22388
|
+
const placed = yield* alertTask("task", { ...request, ...resolved ? { resolved } : undefined });
|
|
22389
|
+
const taskId = placed.taskId === undefined ? undefined : { taskId: placed.taskId };
|
|
22390
|
+
if (placed.joined !== undefined)
|
|
22391
|
+
return {
|
|
22392
|
+
status: "folded",
|
|
22393
|
+
...taskId,
|
|
22394
|
+
joinedRunId: placed.joined.runId,
|
|
22395
|
+
resolvedDuring: false,
|
|
22396
|
+
fix: "none"
|
|
22397
|
+
};
|
|
22398
|
+
if (resolved)
|
|
22399
|
+
return { status: "resolved", ...taskId, resolvedDuring: false, fix: "none" };
|
|
22400
|
+
const conclusion = yield* agentWork("investigate", {
|
|
22401
|
+
agent: "investigator",
|
|
22402
|
+
prompt: investigatePrompt(input.alert),
|
|
22403
|
+
output: Conclusion
|
|
22402
22404
|
});
|
|
22403
|
-
const
|
|
22404
|
-
|
|
22405
|
-
|
|
22406
|
-
|
|
22407
|
-
|
|
22408
|
-
|
|
22409
|
-
const project = input.projectId === undefined ? undefined : { projectId: input.projectId };
|
|
22410
|
-
const ended = (rest) => ({
|
|
22411
|
-
title: plan.title,
|
|
22412
|
-
outOfScope: plan.outOfScope,
|
|
22413
|
-
afterMerge: plan.afterMerge,
|
|
22414
|
-
rejected,
|
|
22415
|
-
tickets: [],
|
|
22416
|
-
deliveries: [],
|
|
22417
|
-
...filed === undefined ? undefined : { tasks: filed.tasks },
|
|
22418
|
-
...docs === undefined ? undefined : { docs },
|
|
22419
|
-
...rest
|
|
22405
|
+
const resolvedDuring = (yield* alertTask("task:check", request)).resolved === true;
|
|
22406
|
+
const stored = yield* artifact("conclusion", {
|
|
22407
|
+
kind: "investigation_conclusion",
|
|
22408
|
+
title: request.title,
|
|
22409
|
+
text: conclusionText(conclusion, resolvedDuring),
|
|
22410
|
+
data: { alert: input.alert, sources: conclusion.sources, confidence: conclusion.confidence }
|
|
22420
22411
|
});
|
|
22421
|
-
|
|
22422
|
-
|
|
22423
|
-
|
|
22424
|
-
|
|
22425
|
-
|
|
22426
|
-
|
|
22427
|
-
|
|
22428
|
-
|
|
22429
|
-
|
|
22430
|
-
|
|
22431
|
-
|
|
22432
|
-
});
|
|
22433
|
-
const choice = menu.find((option) => option === answer) ?? fallback;
|
|
22434
|
-
switch (choice) {
|
|
22435
|
-
case "Finish":
|
|
22436
|
-
return ended({ implemented: false });
|
|
22437
|
-
case "Implement now": {
|
|
22438
|
-
const delivered = yield* implement(input, plan, revision, filed);
|
|
22439
|
-
return { ...ended({ implemented: true }), ...delivered };
|
|
22440
|
-
}
|
|
22441
|
-
case "File as Tasks": {
|
|
22442
|
-
const made = yield* createTasks(`file-tasks:${round}`, { tickets: plan.tickets, ...project });
|
|
22443
|
-
filed = { revision, tasks: made.tasks };
|
|
22444
|
-
break;
|
|
22445
|
-
}
|
|
22446
|
-
case "Second opinion": {
|
|
22447
|
-
opinions++;
|
|
22448
|
-
const opinion = yield* agentWork(`second-opinion:${round}`, {
|
|
22449
|
-
agent: "reviewer",
|
|
22450
|
-
prompt: opinionPrompt(plan),
|
|
22451
|
-
output: Opinion
|
|
22452
|
-
});
|
|
22453
|
-
yield* artifact(`opinion:${round}`, {
|
|
22454
|
-
kind: "plan_review",
|
|
22455
|
-
title: `Second opinion on ${plan.title}`,
|
|
22456
|
-
text: [opinion.summary, ...opinion.concerns.map((concern) => `- ${concern}`)].join(`
|
|
22412
|
+
yield* note("summary", { text: conclusion.summary, artifactId: stored.artifactId });
|
|
22413
|
+
const concluded = {
|
|
22414
|
+
status: "concluded",
|
|
22415
|
+
...taskId,
|
|
22416
|
+
conclusionArtifactId: stored.artifactId,
|
|
22417
|
+
resolvedDuring
|
|
22418
|
+
};
|
|
22419
|
+
if (conclusion.fix === undefined || !conclusion.inCode)
|
|
22420
|
+
return { ...concluded, fix: "none" };
|
|
22421
|
+
const fixRequest = {
|
|
22422
|
+
words: `${conclusion.fix}
|
|
22457
22423
|
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22463
|
-
|
|
22464
|
-
|
|
22465
|
-
|
|
22466
|
-
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
|
|
22471
|
-
|
|
22472
|
-
|
|
22473
|
-
|
|
22474
|
-
const before = plan;
|
|
22475
|
-
revision++;
|
|
22476
|
-
({ plan, rejected } = yield* checked(refined, revision));
|
|
22477
|
-
yield* storePlan(plan, `:${round}`);
|
|
22478
|
-
yield* artifact(`changes:${round}`, {
|
|
22479
|
-
kind: "plan_changes",
|
|
22480
|
-
title: `Revision ${revision} of ${plan.title}`,
|
|
22481
|
-
text: changelog(before, plan, words)
|
|
22482
|
-
});
|
|
22483
|
-
break;
|
|
22484
|
-
}
|
|
22485
|
-
}
|
|
22486
|
-
}
|
|
22487
|
-
if (filed?.revision !== revision)
|
|
22488
|
-
filed = {
|
|
22489
|
-
revision,
|
|
22490
|
-
tasks: (yield* createTasks("file-tasks:last", { tickets: plan.tickets, ...project })).tasks
|
|
22491
|
-
};
|
|
22492
|
-
return ended({
|
|
22493
|
-
implemented: false,
|
|
22494
|
-
stopped: `No decision after ${maxRounds} rounds, so the tickets are filed as Tasks for a person to pick up`
|
|
22424
|
+
${conclusionText(conclusion, resolvedDuring)}`,
|
|
22425
|
+
outcome: "bug"
|
|
22426
|
+
};
|
|
22427
|
+
const confident = conclusion.confidence === "high" && !resolvedDuring;
|
|
22428
|
+
const answer = yield* ask("start-fix", {
|
|
22429
|
+
prompt: `${conclusion.summary}
|
|
22430
|
+
|
|
22431
|
+
Start a Build that fixes it as a bug: ${conclusion.fix}?`,
|
|
22432
|
+
options: [
|
|
22433
|
+
{ value: "Start a fix", description: "A Build fixes it as a bug and opens a ChangeRequest." },
|
|
22434
|
+
{ value: "Not now", description: "Keeps the finding on the Task." }
|
|
22435
|
+
],
|
|
22436
|
+
recommended: confident ? "Start a fix" : "Not now",
|
|
22437
|
+
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.",
|
|
22438
|
+
default: input.autoFix === true && confident ? "Start a fix" : "Not now",
|
|
22439
|
+
timeout: "1 hour"
|
|
22495
22440
|
});
|
|
22441
|
+
if (answer === "Not now")
|
|
22442
|
+
return { ...concluded, fix: "declined", fixRequest };
|
|
22443
|
+
return yield* call("fix", build_default, {
|
|
22444
|
+
...fixRequest,
|
|
22445
|
+
...taskId,
|
|
22446
|
+
...input.repoBindingId === undefined ? undefined : { repoBindingId: input.repoBindingId }
|
|
22447
|
+
}).pipe(map8((fixed) => ({ ...concluded, fix: "started", changeRequestUrl: fixed.changeRequest.url })), catchTag2("ChildFailed", () => succeed7({ ...concluded, fix: "failed", fixRequest })));
|
|
22496
22448
|
})
|
|
22497
22449
|
});
|
|
22498
22450
|
|
|
22499
|
-
// src/
|
|
22500
|
-
var
|
|
22501
|
-
|
|
22502
|
-
|
|
22503
|
-
|
|
22504
|
-
|
|
22451
|
+
// src/plan.ts
|
|
22452
|
+
var Input7 = Struct({
|
|
22453
|
+
words: String5,
|
|
22454
|
+
repoBindingId: optional2(String5),
|
|
22455
|
+
branch: optional2(String5),
|
|
22456
|
+
projectId: optional2(String5),
|
|
22457
|
+
implement: optional2(Boolean3)
|
|
22505
22458
|
});
|
|
22506
|
-
var
|
|
22507
|
-
|
|
22508
|
-
|
|
22509
|
-
|
|
22459
|
+
var Plan = Struct({
|
|
22460
|
+
title: String5,
|
|
22461
|
+
spec: String5,
|
|
22462
|
+
tickets: PlanTickets,
|
|
22463
|
+
outOfScope: ArraySchema(String5),
|
|
22464
|
+
afterMerge: ArraySchema(String5),
|
|
22465
|
+
docs: optional2(Struct({ title: String5, summary: String5 }))
|
|
22510
22466
|
});
|
|
22511
|
-
var
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
22467
|
+
var CriteriaCheck = Struct({
|
|
22468
|
+
alreadyMet: ArraySchema(Struct({ key: String5, criterion: String5 }))
|
|
22469
|
+
});
|
|
22470
|
+
var Rejected = Struct({ key: String5, reason: String5 });
|
|
22471
|
+
var Opinion = Struct({ summary: String5, concerns: ArraySchema(String5) });
|
|
22472
|
+
var TicketResult = Struct({
|
|
22473
|
+
key: String5,
|
|
22474
|
+
title: String5,
|
|
22475
|
+
status: Literals(["merged", "failed", "skipped"]),
|
|
22476
|
+
repoBindingId: optional2(String5),
|
|
22477
|
+
changeRequest: optional2(ChangeRequestResult),
|
|
22478
|
+
reason: optional2(String5)
|
|
22479
|
+
});
|
|
22480
|
+
var Output8 = Struct({
|
|
22481
|
+
title: String5,
|
|
22482
|
+
outOfScope: ArraySchema(String5),
|
|
22483
|
+
afterMerge: ArraySchema(String5),
|
|
22484
|
+
rejected: ArraySchema(Rejected),
|
|
22485
|
+
stopped: optional2(String5),
|
|
22486
|
+
implemented: Boolean3,
|
|
22487
|
+
tickets: ArraySchema(TicketResult),
|
|
22488
|
+
deliveries: ArraySchema(ChangeRequestResult),
|
|
22489
|
+
tasks: optional2(CreateTasksResult.fields.tasks),
|
|
22490
|
+
docs: optional2(ChangeRequestResult)
|
|
22491
|
+
});
|
|
22492
|
+
var menu = ["Implement now", "File as Tasks", "Second opinion", "Refine", "Finish"];
|
|
22493
|
+
var maxRounds = 6;
|
|
22494
|
+
var maxOpinionsPerPlan = 2;
|
|
22495
|
+
var menuDescriptions = {
|
|
22496
|
+
"Implement now": "Builds the tickets in dependency order and delivers them.",
|
|
22497
|
+
"File as Tasks": "Makes a Task of each ticket, to build later.",
|
|
22498
|
+
"Second opinion": "Another Agent reviews the plan.",
|
|
22499
|
+
Refine: "You say what to change and the planner rewrites it.",
|
|
22500
|
+
Finish: "Ends the Run with the plan as it is."
|
|
22501
|
+
};
|
|
22502
|
+
var onFailure = ["Retry with a steer", "Skip it and its dependents", "Stop here"];
|
|
22503
|
+
var ticketRules = [
|
|
22504
|
+
"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.",
|
|
22505
|
+
"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."
|
|
22523
22506
|
].join(`
|
|
22507
|
+
|
|
22524
22508
|
`);
|
|
22525
|
-
var
|
|
22526
|
-
|
|
22527
|
-
|
|
22528
|
-
|
|
22529
|
-
|
|
22509
|
+
var planPrompt = (words) => [
|
|
22510
|
+
`Plan this: ${words}`,
|
|
22511
|
+
"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.",
|
|
22512
|
+
"Then write the spec and the tickets from the goal, the code and the answers.",
|
|
22513
|
+
ticketRules,
|
|
22514
|
+
"List in outOfScope what the plan deliberately leaves out, one item each; each holds the Builds of its tickets.",
|
|
22515
|
+
"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."
|
|
22516
|
+
].join(`
|
|
22530
22517
|
|
|
22531
|
-
|
|
22518
|
+
`);
|
|
22519
|
+
var planText = (plan) => [
|
|
22520
|
+
`# ${plan.title}`,
|
|
22521
|
+
plan.spec,
|
|
22522
|
+
...plan.outOfScope.length === 0 ? [] : ["## Out of scope", plan.outOfScope.map((item) => `- ${item}`).join(`
|
|
22523
|
+
`)],
|
|
22524
|
+
...plan.afterMerge.length === 0 ? [] : ["## After merge", plan.afterMerge.map((item) => `- ${item}`).join(`
|
|
22525
|
+
`)],
|
|
22526
|
+
"## Tickets",
|
|
22527
|
+
...plan.tickets.map(ticketText)
|
|
22528
|
+
].join(`
|
|
22529
|
+
|
|
22530
|
+
`);
|
|
22531
|
+
var ticketText = (ticket) => [
|
|
22532
|
+
`### ${ticket.key}: ${ticket.title}`,
|
|
22533
|
+
ticket.body,
|
|
22534
|
+
...ticket.acceptanceCriteria.map((criterion) => `- [ ] ${criterion}`),
|
|
22535
|
+
...ticket.blockedBy.length === 0 ? [] : [`Blocked by: ${ticket.blockedBy.join(", ")}`]
|
|
22536
|
+
].join(`
|
|
22537
|
+
|
|
22538
|
+
`);
|
|
22539
|
+
var opinionPrompt = (plan) => [
|
|
22540
|
+
"Review this plan as a second opinion. Look for missing scope, tickets too big to review, wrong blockers and risks. Change nothing.",
|
|
22541
|
+
planText(plan)
|
|
22542
|
+
].join(`
|
|
22543
|
+
|
|
22544
|
+
`);
|
|
22545
|
+
var refinePrompt = (plan, words) => [`Change the plan as the user asks: ${words}`, ticketRules, planText(plan)].join(`
|
|
22546
|
+
|
|
22547
|
+
`);
|
|
22548
|
+
var criteriaPrompt2 = (plan) => [
|
|
22549
|
+
"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.",
|
|
22550
|
+
planText(plan)
|
|
22551
|
+
].join(`
|
|
22552
|
+
|
|
22553
|
+
`);
|
|
22554
|
+
var guard2 = " (guard: the base already does this)";
|
|
22555
|
+
var guarded = (plan, check) => {
|
|
22556
|
+
const met = (ticket, criterion) => check.alreadyMet.some((entry) => entry.key === ticket.key && entry.criterion === criterion);
|
|
22557
|
+
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" }] : []);
|
|
22558
|
+
const gone = new Set(rejected.map((entry) => entry.key));
|
|
22559
|
+
const tickets = plan.tickets.flatMap((ticket) => gone.has(ticket.key) ? [] : [
|
|
22560
|
+
{
|
|
22561
|
+
...ticket,
|
|
22562
|
+
acceptanceCriteria: ticket.acceptanceCriteria.map((criterion) => met(ticket, criterion) ? `${criterion}${guard2}` : criterion),
|
|
22563
|
+
blockedBy: ticket.blockedBy.filter((key) => !gone.has(key))
|
|
22564
|
+
}
|
|
22565
|
+
]);
|
|
22566
|
+
return { plan: { ...plan, tickets }, rejected };
|
|
22532
22567
|
};
|
|
22533
|
-
var
|
|
22534
|
-
|
|
22535
|
-
|
|
22536
|
-
|
|
22537
|
-
|
|
22538
|
-
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
|
|
22542
|
-
|
|
22543
|
-
|
|
22544
|
-
|
|
22545
|
-
return {};
|
|
22546
|
-
const written = yield* agentWork("write", { agent: "writer", prompt: prompt3(read), output: Written });
|
|
22547
|
-
const draft = yield* draftUpdate("draft", {
|
|
22548
|
-
projectId,
|
|
22549
|
-
health: written.health,
|
|
22550
|
-
note: noteOf(read, written),
|
|
22551
|
-
writtenFrom: {
|
|
22552
|
-
runs: read.runs.length,
|
|
22553
|
-
taskChanges: read.taskChanges.length,
|
|
22554
|
-
...read.since === undefined ? undefined : { since: read.since }
|
|
22555
|
-
}
|
|
22556
|
-
});
|
|
22557
|
-
return { draftId: draft.id, health: written.health };
|
|
22558
|
-
})
|
|
22559
|
-
});
|
|
22568
|
+
var checked = (plan, revision) => map8(agentWork(`criteria-check:${revision}`, { agent: "reviewer", prompt: criteriaPrompt2(plan), output: CriteriaCheck }), (check) => guarded(plan, check));
|
|
22569
|
+
var storePlan = (plan, suffix) => gen2(function* () {
|
|
22570
|
+
yield* artifact(`spec${suffix}`, {
|
|
22571
|
+
kind: "plan_spec",
|
|
22572
|
+
title: plan.title,
|
|
22573
|
+
text: plan.spec,
|
|
22574
|
+
data: { outOfScope: [...plan.outOfScope] }
|
|
22575
|
+
});
|
|
22576
|
+
yield* artifact(`tickets${suffix}`, {
|
|
22577
|
+
kind: "plan_tickets",
|
|
22578
|
+
title: `Tickets for ${plan.title}`,
|
|
22579
|
+
text: plan.tickets.map(ticketText).join(`
|
|
22560
22580
|
|
|
22561
|
-
|
|
22562
|
-
|
|
22563
|
-
|
|
22564
|
-
|
|
22565
|
-
|
|
22566
|
-
|
|
22567
|
-
|
|
22568
|
-
|
|
22569
|
-
});
|
|
22570
|
-
var Drafted = Struct({
|
|
22571
|
-
tag: String5,
|
|
22572
|
-
previousTag: optional2(String5),
|
|
22573
|
-
title: String5,
|
|
22574
|
-
notes: String5,
|
|
22575
|
-
changeRequests: ArraySchema(String5)
|
|
22576
|
-
});
|
|
22577
|
-
var Output9 = Struct({
|
|
22578
|
-
status: Literals([
|
|
22579
|
-
"gate_failed",
|
|
22580
|
-
"not_mergeable",
|
|
22581
|
-
"rejected",
|
|
22582
|
-
"deployed",
|
|
22583
|
-
"rolled_back",
|
|
22584
|
-
"deploy_failed",
|
|
22585
|
-
"timed_out"
|
|
22586
|
-
]),
|
|
22587
|
-
reason: String5,
|
|
22588
|
-
tag: optional2(String5),
|
|
22589
|
-
notesArtifactId: optional2(String5),
|
|
22590
|
-
revision: optional2(String5),
|
|
22591
|
-
url: optional2(String5)
|
|
22581
|
+
`),
|
|
22582
|
+
data: plan.tickets.map(({ repoBindingId, outcome, ...ticket }) => ({
|
|
22583
|
+
...ticket,
|
|
22584
|
+
acceptanceCriteria: [...ticket.acceptanceCriteria],
|
|
22585
|
+
blockedBy: [...ticket.blockedBy],
|
|
22586
|
+
...repoBindingId === undefined ? undefined : { repoBindingId },
|
|
22587
|
+
...outcome === undefined ? undefined : { outcome }
|
|
22588
|
+
}))
|
|
22589
|
+
});
|
|
22592
22590
|
});
|
|
22593
|
-
var
|
|
22594
|
-
|
|
22595
|
-
|
|
22596
|
-
|
|
22597
|
-
|
|
22598
|
-
|
|
22591
|
+
var changelog = (before, after, words) => {
|
|
22592
|
+
const was = new Map(before.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
22593
|
+
const now = new Map(after.tickets.map((ticket) => [ticket.key, ticketText(ticket)]));
|
|
22594
|
+
const lines = [
|
|
22595
|
+
...after.tickets.filter((ticket) => !was.has(ticket.key)).map((ticket) => `- Added ${ticket.key}: ${ticket.title}`),
|
|
22596
|
+
...before.tickets.filter((ticket) => !now.has(ticket.key)).map((ticket) => `- Removed ${ticket.key}: ${ticket.title}`),
|
|
22597
|
+
...after.tickets.filter((ticket) => was.has(ticket.key) && was.get(ticket.key) !== now.get(ticket.key)).map((ticket) => `- Changed ${ticket.key}: ${ticket.title}`),
|
|
22598
|
+
...before.spec === after.spec ? [] : ["- Changed the spec"],
|
|
22599
|
+
...before.title === after.title ? [] : [`- Renamed the plan from "${before.title}"`]
|
|
22600
|
+
];
|
|
22601
|
+
return [`Asked: ${words}`, ...lines.length === 0 ? ["Nothing changed."] : lines].join(`
|
|
22602
|
+
`);
|
|
22603
|
+
};
|
|
22604
|
+
var buildWords = (ticket) => [
|
|
22605
|
+
ticket.title,
|
|
22606
|
+
ticket.body,
|
|
22607
|
+
...ticket.acceptanceCriteria.length === 0 ? [] : ["Acceptance criteria:", ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)]
|
|
22599
22608
|
].join(`
|
|
22600
22609
|
|
|
22601
22610
|
`);
|
|
22602
|
-
var
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22611
|
+
var settled = (ticket, result) => ({
|
|
22612
|
+
key: ticket.key,
|
|
22613
|
+
title: ticket.title,
|
|
22614
|
+
...result
|
|
22615
|
+
});
|
|
22616
|
+
var buildTicket = (ticket, start, taskId, retry) => gen2(function* () {
|
|
22617
|
+
let head = start;
|
|
22618
|
+
const name = retry === undefined ? ticket.key : `${ticket.key}:${retry.label}`;
|
|
22619
|
+
for (let attempt = 1;; attempt++) {
|
|
22620
|
+
const suffix = attempt === 1 ? "" : `:${attempt}`;
|
|
22621
|
+
const words = retry === undefined ? taskId === undefined ? { words: buildWords(ticket) } : undefined : { words: `${buildWords(ticket)}
|
|
22609
22622
|
|
|
22610
|
-
`
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22614
|
-
|
|
22615
|
-
|
|
22616
|
-
|
|
22617
|
-
|
|
22618
|
-
|
|
22619
|
-
|
|
22620
|
-
|
|
22621
|
-
|
|
22622
|
-
|
|
22623
|
-
|
|
22624
|
-
const failing = read.jobs.filter((job) => job.status === "failed").map((job) => job.name);
|
|
22625
|
-
return {
|
|
22626
|
-
status: "gate_failed",
|
|
22627
|
-
reason: `The pipeline at ${read.head} is ${read.status}${failing.length === 0 ? "" : `: ${failing.join(", ")}`}`,
|
|
22628
|
-
revision: read.head
|
|
22629
|
-
};
|
|
22630
|
-
}
|
|
22631
|
-
const approved = yield* waitFor(at("approved"), {
|
|
22632
|
-
event: "change_request.approval",
|
|
22633
|
-
filter: { changeRequestId, approved: true, head: read.head },
|
|
22634
|
-
timeout: "7 days",
|
|
22635
|
-
or: read.readAt === undefined ? [] : [{ event: "change_request.pipeline", filter: { changeRequestId }, since: read.readAt }]
|
|
22623
|
+
An earlier Build of this failed. ${retry.steer}` };
|
|
22624
|
+
const built = yield* call(`build:${name}${suffix}`, build_default, {
|
|
22625
|
+
...words,
|
|
22626
|
+
...taskId === undefined ? undefined : { taskId },
|
|
22627
|
+
repoBindingId: head.repoBindingId,
|
|
22628
|
+
branch: head.branch,
|
|
22629
|
+
revision: head.revision,
|
|
22630
|
+
land: false
|
|
22631
|
+
}).pipe(catchTag2("ChildFailed", (failure) => succeed7(failure)));
|
|
22632
|
+
if (built instanceof ChildFailed)
|
|
22633
|
+
return settled(ticket, { status: "failed", repoBindingId: head.repoBindingId, reason: built.reason });
|
|
22634
|
+
const merged = yield* mergeChangeRequest(`merge:${name}${suffix}`, {
|
|
22635
|
+
changeRequestId: built.changeRequest.id,
|
|
22636
|
+
head: built.revision
|
|
22636
22637
|
});
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22638
|
+
const delivered = { repoBindingId: head.repoBindingId, changeRequest: built.changeRequest };
|
|
22639
|
+
if (merged.status === "merged")
|
|
22640
|
+
return settled(ticket, { status: "merged", ...delivered });
|
|
22641
|
+
const choice = yield* ask(`conflict:${name}${suffix}`, {
|
|
22642
|
+
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?`,
|
|
22643
|
+
options: [
|
|
22644
|
+
{
|
|
22645
|
+
value: "Rebuild on the integration branch",
|
|
22646
|
+
description: "Builds the ticket again from the branch as it is now."
|
|
22647
|
+
},
|
|
22648
|
+
{ value: "Skip", description: "Leaves it and the tickets it blocks undelivered." }
|
|
22649
|
+
],
|
|
22650
|
+
recommended: "Rebuild on the integration branch",
|
|
22651
|
+
recommendationReason: "A rebuild on the current branch usually resolves a conflict, and skipping also drops every ticket it blocks.",
|
|
22652
|
+
default: "Skip",
|
|
22648
22653
|
timeout: "1 hour"
|
|
22649
22654
|
});
|
|
22650
|
-
if (
|
|
22651
|
-
return {
|
|
22652
|
-
|
|
22653
|
-
|
|
22654
|
-
};
|
|
22655
|
-
return { passed: read.head };
|
|
22656
|
-
}
|
|
22657
|
-
}).pipe(catchTags2({
|
|
22658
|
-
WaitTimedOut: (timedOut) => succeed7({
|
|
22659
|
-
status: "not_mergeable",
|
|
22660
|
-
reason: `Nothing happened on ChangeRequest ${changeRequestId} while waiting for ${timedOut.event}`
|
|
22661
|
-
}),
|
|
22662
|
-
ChangeRequestClosed: (closed) => succeed7({
|
|
22663
|
-
status: "not_mergeable",
|
|
22664
|
-
reason: `ChangeRequest ${changeRequestId} was ${closed.merged ? "merged" : "closed"} before Kitman released it`
|
|
22665
|
-
})
|
|
22666
|
-
}));
|
|
22667
|
-
var recovery = (previous) => previous === undefined ? ["Retry the deploy", "Leave it"] : ["Retry the deploy", `Roll back to ${previous}`, "Leave it"];
|
|
22668
|
-
var release_default = defineLoop({
|
|
22669
|
-
id: "release",
|
|
22670
|
-
version: "2.0.0",
|
|
22671
|
-
title: "Release",
|
|
22672
|
-
input: Input8,
|
|
22673
|
-
output: Output9,
|
|
22674
|
-
agents: { releaser: "releaser" },
|
|
22675
|
-
checkout: "read",
|
|
22676
|
-
capabilities: [
|
|
22677
|
-
"agent_work",
|
|
22678
|
-
"ask",
|
|
22679
|
-
"verify",
|
|
22680
|
-
"claim",
|
|
22681
|
-
"publish_release",
|
|
22682
|
-
"read_change_request",
|
|
22683
|
-
"merge_change_request"
|
|
22684
|
-
],
|
|
22685
|
-
run: (input) => gen2(function* () {
|
|
22686
|
-
const changeRequestId = input.changeRequestId;
|
|
22687
|
-
let gated;
|
|
22688
|
-
if (changeRequestId === undefined) {
|
|
22689
|
-
const gate = yield* verify("gate", input.revision === undefined ? {} : { revision: input.revision });
|
|
22690
|
-
if (!gate.passed) {
|
|
22691
|
-
const failing = gate.checks.filter((check) => check.status !== "passed").map((check) => check.name);
|
|
22692
|
-
return {
|
|
22693
|
-
status: "gate_failed",
|
|
22694
|
-
reason: `The checks fail at ${gate.revision}: ${failing.join(", ")}`,
|
|
22695
|
-
revision: gate.revision
|
|
22696
|
-
};
|
|
22697
|
-
}
|
|
22698
|
-
gated = gate.revision;
|
|
22699
|
-
} else {
|
|
22700
|
-
const ready = yield* readyToMerge(changeRequestId);
|
|
22701
|
-
if (!("passed" in ready))
|
|
22702
|
-
return ready;
|
|
22703
|
-
gated = ready.passed;
|
|
22704
|
-
}
|
|
22705
|
-
let drafted = yield* agentWork("draft-notes", {
|
|
22706
|
-
agent: "releaser",
|
|
22707
|
-
prompt: draftPrompt(input.tag, changeRequestId),
|
|
22708
|
-
output: Drafted
|
|
22655
|
+
if (choice === "Skip")
|
|
22656
|
+
return settled(ticket, { status: "failed", ...delivered, reason: `It conflicted with ${head.branch}` });
|
|
22657
|
+
head = yield* integrationBranch(`rebuild-from:${name}:${attempt + 1}`, {
|
|
22658
|
+
repoBindingId: head.repoBindingId
|
|
22709
22659
|
});
|
|
22710
|
-
|
|
22711
|
-
|
|
22712
|
-
|
|
22713
|
-
|
|
22660
|
+
}
|
|
22661
|
+
});
|
|
22662
|
+
var deliveryBody = (plan, results) => {
|
|
22663
|
+
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}`;
|
|
22664
|
+
const section = (title, status) => {
|
|
22665
|
+
const lines = results.flatMap((result) => result.status === status ? [line(result)] : []);
|
|
22666
|
+
return lines.length === 0 ? [] : [`## ${title}`, lines.join(`
|
|
22667
|
+
`)];
|
|
22668
|
+
};
|
|
22669
|
+
return [
|
|
22670
|
+
`Delivers the plan "${plan.title}".`,
|
|
22671
|
+
...section("Merged", "merged"),
|
|
22672
|
+
...section("Failed", "failed"),
|
|
22673
|
+
...section("Skipped", "skipped")
|
|
22674
|
+
].join(`
|
|
22714
22675
|
|
|
22715
|
-
|
|
22716
|
-
|
|
22717
|
-
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
|
|
22721
|
-
|
|
22722
|
-
|
|
22723
|
-
|
|
22724
|
-
|
|
22725
|
-
|
|
22726
|
-
|
|
22727
|
-
|
|
22728
|
-
|
|
22729
|
-
|
|
22730
|
-
|
|
22731
|
-
|
|
22732
|
-
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
});
|
|
22736
|
-
const kept = yield* artifact("notes", {
|
|
22737
|
-
kind: "release_notes",
|
|
22738
|
-
title: drafted.title,
|
|
22739
|
-
text: drafted.notes,
|
|
22740
|
-
data: { tag, changeRequests: drafted.changeRequests }
|
|
22741
|
-
});
|
|
22742
|
-
return { status: "rejected", reason: why, tag, notesArtifactId: kept.artifactId };
|
|
22743
|
-
}
|
|
22744
|
-
const edit = yield* ask(`edit:${round}`, {
|
|
22745
|
-
prompt: "What should change in the notes?",
|
|
22746
|
-
freeText: true,
|
|
22747
|
-
informationOnly: true,
|
|
22748
|
-
recommendationReason: "You chose to edit them, so only you know what should change.",
|
|
22749
|
-
default: "",
|
|
22750
|
-
timeout: "1 day"
|
|
22751
|
-
});
|
|
22752
|
-
if (edit.trim() === "")
|
|
22753
|
-
continue;
|
|
22754
|
-
drafted = yield* agentWork(`redraft:${round}`, {
|
|
22755
|
-
agent: "releaser",
|
|
22756
|
-
prompt: redraftPrompt(drafted, edit),
|
|
22757
|
-
output: Drafted
|
|
22758
|
-
});
|
|
22676
|
+
`);
|
|
22677
|
+
};
|
|
22678
|
+
var implement = (input, plan, revision, filed) => gen2(function* () {
|
|
22679
|
+
const tasks = filed?.revision === revision ? filed.tasks : (yield* createTasks("tasks", {
|
|
22680
|
+
tickets: plan.tickets,
|
|
22681
|
+
...input.projectId === undefined ? undefined : { projectId: input.projectId }
|
|
22682
|
+
})).tasks;
|
|
22683
|
+
const taskOf = new Map(tasks.map((task) => [task.key, task.taskId]));
|
|
22684
|
+
const results = new Map;
|
|
22685
|
+
const waves = yield* planWaves(plan.tickets);
|
|
22686
|
+
let stopped;
|
|
22687
|
+
for (const [index, wave] of waves.entries()) {
|
|
22688
|
+
const ready = [];
|
|
22689
|
+
for (const ticket of wave) {
|
|
22690
|
+
const unmerged = ticket.blockedBy.find((key) => results.get(key)?.status !== "merged");
|
|
22691
|
+
if (stopped !== undefined)
|
|
22692
|
+
results.set(ticket.key, settled(ticket, { status: "skipped", reason: stopped }));
|
|
22693
|
+
else if (unmerged === undefined)
|
|
22694
|
+
ready.push(ticket);
|
|
22695
|
+
else
|
|
22696
|
+
results.set(ticket.key, settled(ticket, { status: "skipped", reason: `blocked by ${unmerged}, which was not merged` }));
|
|
22759
22697
|
}
|
|
22760
|
-
const
|
|
22761
|
-
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
|
|
22765
|
-
|
|
22766
|
-
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
22771
|
-
|
|
22698
|
+
const repoOf = (ticket) => ticket.repoBindingId ?? input.repoBindingId;
|
|
22699
|
+
const heads = new Map;
|
|
22700
|
+
for (const repo of new Set(ready.map(repoOf)))
|
|
22701
|
+
heads.set(repo, yield* integrationBranch(`integration:wave-${index + 1}:${repo ?? "default"}`, repo === undefined ? {} : { repoBindingId: repo }));
|
|
22702
|
+
const built = yield* forEach2(ready.flatMap((ticket) => {
|
|
22703
|
+
const head = heads.get(repoOf(ticket));
|
|
22704
|
+
return head === undefined ? [] : [buildTicket(ticket, head, taskOf.get(ticket.key))];
|
|
22705
|
+
}), (building) => building, { concurrency: "unbounded" });
|
|
22706
|
+
for (const first of built) {
|
|
22707
|
+
const ticket = ready.find((entry) => entry.key === first.key);
|
|
22708
|
+
let result = first;
|
|
22709
|
+
for (let retry = 1;ticket !== undefined && result.status === "failed" && stopped === undefined; retry++) {
|
|
22710
|
+
const choice = yield* ask(`failed:${ticket.key}:${retry}`, {
|
|
22711
|
+
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?`,
|
|
22712
|
+
options: onFailure,
|
|
22713
|
+
recommended: "Retry with a steer",
|
|
22714
|
+
recommendationReason: "The reason it failed is above, so a steer can aim the retry at it; skipping also drops the tickets it blocks.",
|
|
22715
|
+
default: "Skip it and its dependents",
|
|
22716
|
+
timeout: "1 hour"
|
|
22772
22717
|
});
|
|
22773
|
-
if (
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
tag,
|
|
22778
|
-
notesArtifactId: notes.artifactId
|
|
22779
|
-
};
|
|
22780
|
-
if (merged.status === "merged") {
|
|
22781
|
-
revision = merged.revision;
|
|
22718
|
+
if (choice === "Skip it and its dependents")
|
|
22719
|
+
break;
|
|
22720
|
+
if (choice === "Stop here") {
|
|
22721
|
+
stopped = `Stopped after ${ticket.key} failed`;
|
|
22782
22722
|
break;
|
|
22783
22723
|
}
|
|
22784
|
-
|
|
22785
|
-
|
|
22786
|
-
|
|
22787
|
-
|
|
22788
|
-
|
|
22789
|
-
|
|
22790
|
-
|
|
22791
|
-
const
|
|
22792
|
-
|
|
22793
|
-
|
|
22794
|
-
|
|
22724
|
+
const steer = yield* ask(`steer:${ticket.key}:${retry}`, {
|
|
22725
|
+
prompt: `What should the retry of ${ticket.key} do differently?`,
|
|
22726
|
+
freeText: true,
|
|
22727
|
+
recommended: `Address the failure: ${result.reason ?? "find out why it failed first"}`,
|
|
22728
|
+
recommendationReason: "Pointing the retry at the reason it failed is the least a steer should do.",
|
|
22729
|
+
default: null
|
|
22730
|
+
});
|
|
22731
|
+
const head = yield* integrationBranch(`integration:${ticket.key}:retry-${retry}`, {
|
|
22732
|
+
...repoOf(ticket) === undefined ? undefined : { repoBindingId: repoOf(ticket) }
|
|
22733
|
+
});
|
|
22734
|
+
result = yield* buildTicket(ticket, head, taskOf.get(ticket.key), { label: `retry-${retry}`, steer });
|
|
22795
22735
|
}
|
|
22736
|
+
results.set(result.key, result);
|
|
22796
22737
|
}
|
|
22797
|
-
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
|
|
22803
|
-
|
|
22738
|
+
}
|
|
22739
|
+
const ordered = plan.tickets.flatMap((ticket) => results.get(ticket.key) ?? []);
|
|
22740
|
+
const repos = [
|
|
22741
|
+
...new Set(ordered.flatMap((result) => result.status === "merged" ? result.repoBindingId ?? [] : []))
|
|
22742
|
+
];
|
|
22743
|
+
const deliveries = yield* forEach2(repos, (repo) => changeRequest(`deliver:${repo}`, {
|
|
22744
|
+
title: plan.title,
|
|
22745
|
+
body: deliveryBody(plan, ordered.filter((result) => result.repoBindingId === repo || result.repoBindingId === undefined)),
|
|
22746
|
+
integration: repo
|
|
22747
|
+
}));
|
|
22748
|
+
return { tickets: ordered, deliveries, tasks, ...stopped === undefined ? undefined : { stopped } };
|
|
22749
|
+
});
|
|
22750
|
+
var plan_default = defineLoop({
|
|
22751
|
+
id: "plan",
|
|
22752
|
+
version: "2.0.0",
|
|
22753
|
+
title: "Plan",
|
|
22754
|
+
input: Input7,
|
|
22755
|
+
output: Output8,
|
|
22756
|
+
agents: { planner: "planner", reviewer: "reviewer" },
|
|
22757
|
+
checkout: "write",
|
|
22758
|
+
capabilities: [
|
|
22759
|
+
"agent_work",
|
|
22760
|
+
"ask",
|
|
22761
|
+
"call",
|
|
22762
|
+
"verify",
|
|
22763
|
+
"change_request",
|
|
22764
|
+
"write_back",
|
|
22765
|
+
"create_tasks",
|
|
22766
|
+
"merge_change_request",
|
|
22767
|
+
"read_change_request",
|
|
22768
|
+
"post_comment"
|
|
22769
|
+
],
|
|
22770
|
+
calls: () => [build_default],
|
|
22771
|
+
run: (input) => gen2(function* () {
|
|
22772
|
+
const drafted = yield* agentWorkOnce("plan", {
|
|
22773
|
+
agent: "planner",
|
|
22774
|
+
skill: "grill-with-docs",
|
|
22775
|
+
prompt: planPrompt(input.words),
|
|
22776
|
+
output: Plan
|
|
22804
22777
|
});
|
|
22805
|
-
const
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22778
|
+
const docs = drafted.docs === undefined ? undefined : yield* changeRequest("docs", { title: drafted.docs.title, body: drafted.docs.summary });
|
|
22779
|
+
let revision = 1;
|
|
22780
|
+
let { plan, rejected } = yield* checked(drafted, revision);
|
|
22781
|
+
let filed;
|
|
22782
|
+
let opinions = 0;
|
|
22783
|
+
yield* storePlan(plan, "");
|
|
22784
|
+
const project = input.projectId === undefined ? undefined : { projectId: input.projectId };
|
|
22785
|
+
const ended = (rest) => ({
|
|
22786
|
+
title: plan.title,
|
|
22787
|
+
outOfScope: plan.outOfScope,
|
|
22788
|
+
afterMerge: plan.afterMerge,
|
|
22789
|
+
rejected,
|
|
22790
|
+
tickets: [],
|
|
22791
|
+
deliveries: [],
|
|
22792
|
+
...filed === undefined ? undefined : { tasks: filed.tasks },
|
|
22793
|
+
...docs === undefined ? undefined : { docs },
|
|
22794
|
+
...rest
|
|
22795
|
+
});
|
|
22796
|
+
for (let round = 1;round <= maxRounds; round++) {
|
|
22797
|
+
const isFiled = filed?.revision === revision;
|
|
22798
|
+
const options = menu.filter((option) => !(option === "File as Tasks" && isFiled) && !(option === "Second opinion" && opinions >= maxOpinionsPerPlan));
|
|
22799
|
+
const fallback = input.implement === true ? "Implement now" : isFiled ? "Finish" : "File as Tasks";
|
|
22800
|
+
const answer = yield* ask(`next:${round}`, {
|
|
22801
|
+
prompt: `What next for the plan "${plan.title}"?`,
|
|
22802
|
+
options: options.map((option) => ({ value: option, description: menuDescriptions[option] })),
|
|
22803
|
+
recommended: fallback,
|
|
22804
|
+
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.",
|
|
22805
|
+
default: fallback,
|
|
22820
22806
|
timeout: "1 hour"
|
|
22821
22807
|
});
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
22826
|
-
|
|
22827
|
-
|
|
22808
|
+
const choice = menu.find((option) => option === answer) ?? fallback;
|
|
22809
|
+
switch (choice) {
|
|
22810
|
+
case "Finish":
|
|
22811
|
+
return ended({ implemented: false });
|
|
22812
|
+
case "Implement now": {
|
|
22813
|
+
const delivered = yield* implement(input, plan, revision, filed);
|
|
22814
|
+
return { ...ended({ implemented: true }), ...delivered };
|
|
22815
|
+
}
|
|
22816
|
+
case "File as Tasks": {
|
|
22817
|
+
const made = yield* createTasks(`file-tasks:${round}`, { tickets: plan.tickets, ...project });
|
|
22818
|
+
filed = { revision, tasks: made.tasks };
|
|
22819
|
+
break;
|
|
22820
|
+
}
|
|
22821
|
+
case "Second opinion": {
|
|
22822
|
+
opinions++;
|
|
22823
|
+
const opinion = yield* agentWork(`second-opinion:${round}`, {
|
|
22824
|
+
agent: "reviewer",
|
|
22825
|
+
prompt: opinionPrompt(plan),
|
|
22826
|
+
output: Opinion
|
|
22827
|
+
});
|
|
22828
|
+
yield* artifact(`opinion:${round}`, {
|
|
22829
|
+
kind: "plan_review",
|
|
22830
|
+
title: `Second opinion on ${plan.title}`,
|
|
22831
|
+
text: [opinion.summary, ...opinion.concerns.map((concern) => `- ${concern}`)].join(`
|
|
22832
|
+
|
|
22833
|
+
`)
|
|
22834
|
+
});
|
|
22835
|
+
break;
|
|
22836
|
+
}
|
|
22837
|
+
case "Refine": {
|
|
22838
|
+
const words = yield* ask(`refine:${round}`, {
|
|
22839
|
+
prompt: "What should change in the plan?",
|
|
22840
|
+
freeText: true,
|
|
22841
|
+
informationOnly: true,
|
|
22842
|
+
recommendationReason: "You chose to refine it, so only you know what should change."
|
|
22843
|
+
});
|
|
22844
|
+
const refined = yield* agentWorkOnce(`refine-plan:${round}`, {
|
|
22845
|
+
agent: "planner",
|
|
22846
|
+
prompt: refinePrompt(plan, words),
|
|
22847
|
+
output: Plan
|
|
22848
|
+
});
|
|
22849
|
+
const before = plan;
|
|
22850
|
+
revision++;
|
|
22851
|
+
({ plan, rejected } = yield* checked(refined, revision));
|
|
22852
|
+
yield* storePlan(plan, `:${round}`);
|
|
22853
|
+
yield* artifact(`changes:${round}`, {
|
|
22854
|
+
kind: "plan_changes",
|
|
22855
|
+
title: `Revision ${revision} of ${plan.title}`,
|
|
22856
|
+
text: changelog(before, plan, words)
|
|
22857
|
+
});
|
|
22858
|
+
break;
|
|
22859
|
+
}
|
|
22828
22860
|
}
|
|
22829
|
-
const previous = drafted.previousTag ?? tag;
|
|
22830
|
-
yield* rerunPipeline(`deploy:redeploy-${round}`, { ref: previous, all: true });
|
|
22831
|
-
const back = yield* deployed(`deploy:rollback-${round}`, previous);
|
|
22832
|
-
if (back === "deployed")
|
|
22833
|
-
return { status: "rolled_back", reason: `Rolled back to ${previous}`, ...settled };
|
|
22834
|
-
status = back;
|
|
22835
22861
|
}
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22862
|
+
if (filed?.revision !== revision)
|
|
22863
|
+
filed = {
|
|
22864
|
+
revision,
|
|
22865
|
+
tasks: (yield* createTasks("file-tasks:last", { tickets: plan.tickets, ...project })).tasks
|
|
22866
|
+
};
|
|
22867
|
+
return ended({
|
|
22868
|
+
implemented: false,
|
|
22869
|
+
stopped: `No decision after ${maxRounds} rounds, so the tickets are filed as Tasks for a person to pick up`
|
|
22841
22870
|
});
|
|
22842
|
-
|
|
22871
|
+
})
|
|
22872
|
+
});
|
|
22873
|
+
|
|
22874
|
+
// src/project-update.ts
|
|
22875
|
+
var Health2 = Literals(["on_track", "at_risk", "off_track"]);
|
|
22876
|
+
var Input8 = Struct({ projectId: String5 });
|
|
22877
|
+
var Output9 = Struct({
|
|
22878
|
+
draftId: optional2(String5),
|
|
22879
|
+
health: optional2(Health2)
|
|
22880
|
+
});
|
|
22881
|
+
var Written = Struct({
|
|
22882
|
+
health: Health2,
|
|
22883
|
+
note: String5,
|
|
22884
|
+
healthReason: optional2(String5)
|
|
22885
|
+
});
|
|
22886
|
+
var label2 = { on_track: "on track", at_risk: "at risk", off_track: "off track" };
|
|
22887
|
+
var day = (iso) => new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
22888
|
+
var prompt3 = (read) => [
|
|
22889
|
+
`Write a Project Update for ${read.project.name} from what happened ${read.since === undefined ? "so far" : `since its last Update on ${day(read.since)}`}.`,
|
|
22890
|
+
"```json",
|
|
22891
|
+
JSON.stringify({ health: read.health, runs: read.runs, taskChanges: read.taskChanges }, null, 2),
|
|
22892
|
+
"```",
|
|
22893
|
+
"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.",
|
|
22894
|
+
...read.health === undefined ? [] : [
|
|
22895
|
+
`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.`
|
|
22896
|
+
],
|
|
22897
|
+
"A person reviews and posts it, so claim nothing the Runs and Task changes do not show."
|
|
22898
|
+
].join(`
|
|
22899
|
+
`);
|
|
22900
|
+
var noteOf = (read, written) => {
|
|
22901
|
+
if (read.health === undefined || read.health === written.health)
|
|
22902
|
+
return written.note;
|
|
22903
|
+
const moved = `Health moved from ${label2[read.health]} to ${label2[written.health]}`;
|
|
22904
|
+
return `${written.healthReason === undefined ? `${moved}.` : `${moved}: ${written.healthReason}`}
|
|
22905
|
+
|
|
22906
|
+
${written.note}`;
|
|
22907
|
+
};
|
|
22908
|
+
var project_update_default = defineLoop({
|
|
22909
|
+
id: "project-update",
|
|
22910
|
+
version: "2.0.1",
|
|
22911
|
+
title: "Project update",
|
|
22912
|
+
input: Input8,
|
|
22913
|
+
output: Output9,
|
|
22914
|
+
agents: { writer: "writer" },
|
|
22915
|
+
checkout: "none",
|
|
22916
|
+
capabilities: ["read_project", "agent_work", "draft_update"],
|
|
22917
|
+
run: ({ projectId }) => gen2(function* () {
|
|
22918
|
+
const read = yield* projectActivity("read", { projectId });
|
|
22919
|
+
if (read.runs.length === 0 && read.taskChanges.length === 0)
|
|
22920
|
+
return {};
|
|
22921
|
+
const written = yield* agentWork("write", { agent: "writer", prompt: prompt3(read), output: Written });
|
|
22922
|
+
const draft = yield* draftUpdate("draft", {
|
|
22923
|
+
projectId,
|
|
22924
|
+
health: written.health,
|
|
22925
|
+
note: noteOf(read, written),
|
|
22926
|
+
writtenFrom: {
|
|
22927
|
+
runs: read.runs.length,
|
|
22928
|
+
taskChanges: read.taskChanges.length,
|
|
22929
|
+
...read.since === undefined ? undefined : { since: read.since }
|
|
22930
|
+
}
|
|
22931
|
+
});
|
|
22932
|
+
return { draftId: draft.id, health: written.health };
|
|
22843
22933
|
})
|
|
22844
22934
|
});
|
|
22845
22935
|
|
|
@@ -23638,6 +23728,11 @@ var kitman_config_default = defineKit({
|
|
|
23638
23728
|
skills: ["git-workflow-and-versioning", "shipping-and-launch"],
|
|
23639
23729
|
instructions: { file: "agents/releaser.md" }
|
|
23640
23730
|
},
|
|
23731
|
+
deployer: {
|
|
23732
|
+
effort: "low",
|
|
23733
|
+
fallbacks,
|
|
23734
|
+
instructions: { file: "agents/deployer.md" }
|
|
23735
|
+
},
|
|
23641
23736
|
investigator: {
|
|
23642
23737
|
effort: "high",
|
|
23643
23738
|
fallbacks,
|
|
@@ -26265,6 +26360,7 @@ class RunnerCapabilityRpcs extends make30(make29("PutQuestion", {
|
|
|
26265
26360
|
informationOnly: optional2(Boolean3),
|
|
26266
26361
|
verification: optional2(SuggestedVerification),
|
|
26267
26362
|
default: optional2(String5),
|
|
26363
|
+
auto: optional2(String5),
|
|
26268
26364
|
timeoutMillis: optional2(Finite),
|
|
26269
26365
|
deferredName: String5
|
|
26270
26366
|
},
|
|
@@ -26776,6 +26872,7 @@ var runnerHost = (capabilities) => ({
|
|
|
26776
26872
|
...launch.options === undefined ? undefined : { options: launch.options },
|
|
26777
26873
|
...launch.verification === undefined ? undefined : { verification: launch.verification },
|
|
26778
26874
|
...launch.default === undefined ? undefined : { default: launch.default },
|
|
26875
|
+
...launch.auto === undefined ? undefined : { auto: launch.auto },
|
|
26779
26876
|
...launch.timeoutMillis === undefined ? undefined : { timeoutMillis: launch.timeoutMillis }
|
|
26780
26877
|
})).pipe(asVoid2),
|
|
26781
26878
|
startVerification: (launch) => requested(launch.step, capabilities.startVerification({
|
|
@@ -26997,6 +27094,7 @@ var rpcProgramPorts = (rpc, work) => {
|
|
|
26997
27094
|
...question.options === undefined ? undefined : { options: [...question.options] },
|
|
26998
27095
|
...question.verification === undefined ? undefined : { verification: question.verification },
|
|
26999
27096
|
...question.default === undefined ? undefined : { default: question.default },
|
|
27097
|
+
...question.auto === undefined ? undefined : { auto: question.auto },
|
|
27000
27098
|
...question.timeoutMillis === undefined ? undefined : { timeoutMillis: question.timeoutMillis }
|
|
27001
27099
|
}),
|
|
27002
27100
|
startVerification: (verification) => rpc.startVerification({
|
|
@@ -29298,4 +29396,4 @@ var runKit = (config, identity, packages) => {
|
|
|
29298
29396
|
runMain2(program.pipe(flatMap3((code) => sync2(() => process.exit(code)))));
|
|
29299
29397
|
};
|
|
29300
29398
|
// .kitman-build-entry.ts
|
|
29301
|
-
runKit(kitman_config_default, { name: "@kitman-sh/software-engineering", version: "0.10.
|
|
29399
|
+
runKit(kitman_config_default, { name: "@kitman-sh/software-engineering", version: "0.10.59" }, []);
|