@joynt/foundry 0.0.0-stage → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backends/command.d.ts +37 -0
- package/dist/backends/command.d.ts.map +1 -0
- package/dist/backends/command.js +79 -0
- package/dist/backends/command.js.map +1 -0
- package/dist/backends/index.d.ts +9 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +37 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +191 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/action.d.ts +6 -0
- package/dist/commands/action.d.ts.map +1 -0
- package/dist/commands/action.js +171 -0
- package/dist/commands/action.js.map +1 -0
- package/dist/commands/cancelled.d.ts +5 -0
- package/dist/commands/cancelled.d.ts.map +1 -0
- package/dist/commands/cancelled.js +107 -0
- package/dist/commands/cancelled.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +323 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/preview.d.ts +4 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +108 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.d.ts.map +1 -0
- package/dist/commands/release.js +89 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/commands/report.d.ts +14 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +91 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/reset.d.ts +4 -0
- package/dist/commands/reset.d.ts.map +1 -0
- package/dist/commands/reset.js +155 -0
- package/dist/commands/reset.js.map +1 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +73 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/setup-bot.d.ts +31 -0
- package/dist/commands/setup-bot.d.ts.map +1 -0
- package/dist/commands/setup-bot.js +429 -0
- package/dist/commands/setup-bot.js.map +1 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +77 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +147 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent-output.d.ts +49 -0
- package/dist/lib/agent-output.d.ts.map +1 -0
- package/dist/lib/agent-output.js +256 -0
- package/dist/lib/agent-output.js.map +1 -0
- package/dist/lib/agent-report.d.ts +107 -0
- package/dist/lib/agent-report.d.ts.map +1 -0
- package/dist/lib/agent-report.js +233 -0
- package/dist/lib/agent-report.js.map +1 -0
- package/dist/lib/authorize.d.ts +3 -0
- package/dist/lib/authorize.d.ts.map +1 -0
- package/dist/lib/authorize.js +67 -0
- package/dist/lib/authorize.js.map +1 -0
- package/dist/lib/branches.d.ts +22 -0
- package/dist/lib/branches.d.ts.map +1 -0
- package/dist/lib/branches.js +82 -0
- package/dist/lib/branches.js.map +1 -0
- package/dist/lib/claim.d.ts +22 -0
- package/dist/lib/claim.d.ts.map +1 -0
- package/dist/lib/claim.js +129 -0
- package/dist/lib/claim.js.map +1 -0
- package/dist/lib/completion.d.ts +5 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +63 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/deployments.d.ts +21 -0
- package/dist/lib/deployments.d.ts.map +1 -0
- package/dist/lib/deployments.js +104 -0
- package/dist/lib/deployments.js.map +1 -0
- package/dist/lib/event-handler.d.ts +74 -0
- package/dist/lib/event-handler.d.ts.map +1 -0
- package/dist/lib/event-handler.js +1002 -0
- package/dist/lib/event-handler.js.map +1 -0
- package/dist/lib/events.d.ts +72 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/events.js +3 -0
- package/dist/lib/events.js.map +1 -0
- package/dist/lib/gh-cli-client.d.ts +46 -0
- package/dist/lib/gh-cli-client.d.ts.map +1 -0
- package/dist/lib/gh-cli-client.js +260 -0
- package/dist/lib/gh-cli-client.js.map +1 -0
- package/dist/lib/git.d.ts +42 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +247 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/github-client.d.ts +98 -0
- package/dist/lib/github-client.d.ts.map +1 -0
- package/dist/lib/github-client.js +3 -0
- package/dist/lib/github-client.js.map +1 -0
- package/dist/lib/github.d.ts +82 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +202 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/issue-file.d.ts +15 -0
- package/dist/lib/issue-file.d.ts.map +1 -0
- package/dist/lib/issue-file.js +84 -0
- package/dist/lib/issue-file.js.map +1 -0
- package/dist/lib/labels.d.ts +14 -0
- package/dist/lib/labels.d.ts.map +1 -0
- package/dist/lib/labels.js +70 -0
- package/dist/lib/labels.js.map +1 -0
- package/dist/lib/log.d.ts +11 -0
- package/dist/lib/log.d.ts.map +1 -0
- package/dist/lib/log.js +53 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/octokit-client.d.ts +112 -0
- package/dist/lib/octokit-client.d.ts.map +1 -0
- package/dist/lib/octokit-client.js +507 -0
- package/dist/lib/octokit-client.js.map +1 -0
- package/dist/lib/preview-verify.d.ts +48 -0
- package/dist/lib/preview-verify.d.ts.map +1 -0
- package/dist/lib/preview-verify.js +148 -0
- package/dist/lib/preview-verify.js.map +1 -0
- package/dist/lib/preview.d.ts +33 -0
- package/dist/lib/preview.d.ts.map +1 -0
- package/dist/lib/preview.js +308 -0
- package/dist/lib/preview.js.map +1 -0
- package/dist/lib/reconcile.d.ts +14 -0
- package/dist/lib/reconcile.d.ts.map +1 -0
- package/dist/lib/reconcile.js +114 -0
- package/dist/lib/reconcile.js.map +1 -0
- package/dist/lib/review.d.ts +38 -0
- package/dist/lib/review.d.ts.map +1 -0
- package/dist/lib/review.js +284 -0
- package/dist/lib/review.js.map +1 -0
- package/dist/lib/stale-prs.d.ts +31 -0
- package/dist/lib/stale-prs.d.ts.map +1 -0
- package/dist/lib/stale-prs.js +161 -0
- package/dist/lib/stale-prs.js.map +1 -0
- package/dist/lib/state-store.d.ts +23 -0
- package/dist/lib/state-store.d.ts.map +1 -0
- package/dist/lib/state-store.js +3 -0
- package/dist/lib/state-store.js.map +1 -0
- package/dist/lib/state.d.ts +23 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +57 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/status-comment.d.ts +14 -0
- package/dist/lib/status-comment.d.ts.map +1 -0
- package/dist/lib/status-comment.js +195 -0
- package/dist/lib/status-comment.js.map +1 -0
- package/dist/lib/stores/file-store.d.ts +22 -0
- package/dist/lib/stores/file-store.d.ts.map +1 -0
- package/dist/lib/stores/file-store.js +172 -0
- package/dist/lib/stores/file-store.js.map +1 -0
- package/dist/lib/stores/index.d.ts +13 -0
- package/dist/lib/stores/index.d.ts.map +1 -0
- package/dist/lib/stores/index.js +20 -0
- package/dist/lib/stores/index.js.map +1 -0
- package/dist/lib/task-recovery.d.ts +22 -0
- package/dist/lib/task-recovery.d.ts.map +1 -0
- package/dist/lib/task-recovery.js +172 -0
- package/dist/lib/task-recovery.js.map +1 -0
- package/dist/lib/templates.d.ts +8 -0
- package/dist/lib/templates.d.ts.map +1 -0
- package/dist/lib/templates.js +58 -0
- package/dist/lib/templates.js.map +1 -0
- package/dist/lib/thread.d.ts +13 -0
- package/dist/lib/thread.d.ts.map +1 -0
- package/dist/lib/thread.js +75 -0
- package/dist/lib/thread.js.map +1 -0
- package/dist/lib/verify.d.ts +31 -0
- package/dist/lib/verify.d.ts.map +1 -0
- package/dist/lib/verify.js +90 -0
- package/dist/lib/verify.js.map +1 -0
- package/dist/lib/version.d.ts +9 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +134 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/webhook.d.ts +39 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/lib/webhook.js +248 -0
- package/dist/lib/webhook.js.map +1 -0
- package/dist/lib/worker.d.ts +42 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +9 -0
- package/dist/lib/worker.js.map +1 -0
- package/dist/lib/workers/index.d.ts +11 -0
- package/dist/lib/workers/index.d.ts.map +1 -0
- package/dist/lib/workers/index.js +18 -0
- package/dist/lib/workers/index.js.map +1 -0
- package/dist/lib/workers/subprocess.d.ts +8 -0
- package/dist/lib/workers/subprocess.d.ts.map +1 -0
- package/dist/lib/workers/subprocess.js +105 -0
- package/dist/lib/workers/subprocess.js.map +1 -0
- package/dist/lib/worktree.d.ts +21 -0
- package/dist/lib/worktree.d.ts.map +1 -0
- package/dist/lib/worktree.js +113 -0
- package/dist/lib/worktree.js.map +1 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -4
- package/templates/foundry.yml +121 -0
- package/templates/vercel-preview-url.sh +61 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EventHandler = exports.FAILED_FOOTER = void 0;
|
|
37
|
+
exports.buildResumeContext = buildResumeContext;
|
|
38
|
+
exports.buildPRBody = buildPRBody;
|
|
39
|
+
/**
|
|
40
|
+
* EventHandler — processes FoundryEvent instances.
|
|
41
|
+
*
|
|
42
|
+
* All behavior lives here; `commands/action.ts` is composition only. Humans
|
|
43
|
+
* drive a task with three commands (`go`, `restart`, `stop`); everything
|
|
44
|
+
* else that moves a task is a review, a merge, or the agent finishing.
|
|
45
|
+
*/
|
|
46
|
+
const github = __importStar(require("./github.js"));
|
|
47
|
+
const git = __importStar(require("./git.js"));
|
|
48
|
+
const index_js_1 = require("./workers/index.js");
|
|
49
|
+
const state = __importStar(require("./state.js"));
|
|
50
|
+
const claim = __importStar(require("./claim.js"));
|
|
51
|
+
const labels_js_1 = require("./labels.js");
|
|
52
|
+
const log = __importStar(require("./log.js"));
|
|
53
|
+
const verify = __importStar(require("./verify.js"));
|
|
54
|
+
const agentOutput = __importStar(require("./agent-output.js"));
|
|
55
|
+
const agent_report_js_1 = require("./agent-report.js");
|
|
56
|
+
const worktree_js_1 = require("./worktree.js");
|
|
57
|
+
const branches_js_1 = require("./branches.js");
|
|
58
|
+
const issue_file_js_1 = require("./issue-file.js");
|
|
59
|
+
const thread_js_1 = require("./thread.js");
|
|
60
|
+
const previewVerify = __importStar(require("./preview-verify.js"));
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
const stalePRs = __importStar(require("./stale-prs.js"));
|
|
63
|
+
const review = __importStar(require("./review.js"));
|
|
64
|
+
const index_js_2 = require("../backends/index.js");
|
|
65
|
+
const preview = __importStar(require("./preview.js"));
|
|
66
|
+
const statusComment = __importStar(require("./status-comment.js"));
|
|
67
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
68
|
+
const PREVIEW_VERIFY_MARKER = '<!-- foundry-preview-verify -->';
|
|
69
|
+
/** What every non-success ending tells the human. */
|
|
70
|
+
exports.FAILED_FOOTER = '`@foundry go` to continue from the branch · `@foundry restart` to start over';
|
|
71
|
+
function workOnBranch(task, preserved) {
|
|
72
|
+
return preserved.pushed ? [`_Work so far is on \`${task.branch}\`._`] : [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What a relaunched agent is told about why it is being launched again. Goes
|
|
76
|
+
* in FOUNDRY_RESUME_CONTEXT; the Claude preset's prompt includes it.
|
|
77
|
+
*/
|
|
78
|
+
function buildResumeContext(task, humanText, kind) {
|
|
79
|
+
const lastReport = task.last_agent_message ? ` Your last report: "${task.last_agent_message}".` : '';
|
|
80
|
+
const prNote = task.pr_url ? ` Your pull request ${task.pr_url} is open; commits you add to the branch update it.` : '';
|
|
81
|
+
const intro = {
|
|
82
|
+
answer: `You stopped earlier because you needed a decision from a human${task.last_agent_message ? `; you reported: "${task.last_agent_message}"` : ' (your questions are in your last status comment on the issue)'}. The human replied:`,
|
|
83
|
+
plan: `You posted a plan for this issue and the human reviewed it. Their reply:`,
|
|
84
|
+
review: `A reviewer requested changes on your pull request:`,
|
|
85
|
+
go: `The human commented \`@foundry go\` on the issue.${prNote}${lastReport} Their message:`,
|
|
86
|
+
}[kind];
|
|
87
|
+
const outro = {
|
|
88
|
+
answer: 'Apply the reply and continue the task.',
|
|
89
|
+
plan: 'If the reply approves the plan, implement it now; if it asks for changes, revise the plan and report plan_ready again.',
|
|
90
|
+
review: 'Address the review on this branch and commit.',
|
|
91
|
+
go: 'The issue file holds the whole thread, including every comment since your last launch; read it, then finish the task from the branch and report.',
|
|
92
|
+
}[kind];
|
|
93
|
+
const text = humanText.trim() || '(no message; see the thread in the issue file)';
|
|
94
|
+
return [
|
|
95
|
+
`RESUMING issue #${task.issue} on branch ${task.branch}. The branch already holds your earlier commits; continue from its current state, do not start over.`,
|
|
96
|
+
'',
|
|
97
|
+
intro,
|
|
98
|
+
'',
|
|
99
|
+
text,
|
|
100
|
+
'',
|
|
101
|
+
outro,
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
/** The PR description: what the agent says it did and what it assumed, for the reviewer. */
|
|
105
|
+
function buildPRBody(task, outcome) {
|
|
106
|
+
const parts = [`Closes #${task.issue}`, ''];
|
|
107
|
+
const report = outcome?.report;
|
|
108
|
+
if (report) {
|
|
109
|
+
parts.push('## Summary', '', report.summary, '');
|
|
110
|
+
if (report.assumptions && report.assumptions.length > 0) {
|
|
111
|
+
parts.push('## Assumptions', '', ...report.assumptions.map(a => `- ${a}`), '');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
parts.push('---', `Opened by Foundry (${task.agent_backend}). Request changes on this PR, or comment \`@foundry go <feedback>\` on the issue, to send the agent back to work.`);
|
|
115
|
+
return parts.join('\n');
|
|
116
|
+
}
|
|
117
|
+
function resolvePermissionMode(issueLabels, config) {
|
|
118
|
+
const modeLabels = config.mode_labels ?? { plan: 'mode:plan', auto: 'mode:auto', default: 'mode:default' };
|
|
119
|
+
if (issueLabels.includes(modeLabels.plan))
|
|
120
|
+
return '--permission-mode plan';
|
|
121
|
+
if (issueLabels.includes(modeLabels.default))
|
|
122
|
+
return '--permission-mode default';
|
|
123
|
+
return '--dangerously-skip-permissions';
|
|
124
|
+
}
|
|
125
|
+
function getWorker(_config) {
|
|
126
|
+
return (0, index_js_1.resolveWorker)('subprocess');
|
|
127
|
+
}
|
|
128
|
+
async function postToConversationTarget(config, task, body) {
|
|
129
|
+
if (task.pr_number) {
|
|
130
|
+
await github.commentOnPR(config.repo, task.pr_number, body);
|
|
131
|
+
}
|
|
132
|
+
else if (task.pr_url) {
|
|
133
|
+
const prNumber = github.extractPRNumber(task.pr_url);
|
|
134
|
+
if (prNumber) {
|
|
135
|
+
await github.commentOnPR(config.repo, prNumber, body);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await github.addComment(config.repo, task.issue, body);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
await github.addComment(config.repo, task.issue, body);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function triggerPreviewUp(config, task) {
|
|
146
|
+
if (!config.preview)
|
|
147
|
+
return;
|
|
148
|
+
const fresh = state.getAllTasks(config.repo).find(t => t.issue === task.issue);
|
|
149
|
+
if (fresh) {
|
|
150
|
+
try {
|
|
151
|
+
await preview.previewUp(config, fresh);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
log.warn(`Preview up failed for #${task.issue}: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function triggerPreviewDown(config, task) {
|
|
159
|
+
if (!config.preview)
|
|
160
|
+
return;
|
|
161
|
+
if (!task.preview_url)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
await preview.previewDown(config, task);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
log.warn(`Preview down failed for #${task.issue}: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** A task whose job may still hold a live agent process. */
|
|
171
|
+
function mayBeRunning(task) {
|
|
172
|
+
return task.status !== 'claimed' && task.status !== 'done' && task.status !== 'failed' && task.status !== 'stopped';
|
|
173
|
+
}
|
|
174
|
+
// ── EventHandler ─────────────────────────────────────────────────────────
|
|
175
|
+
class EventHandler {
|
|
176
|
+
config;
|
|
177
|
+
repoDir;
|
|
178
|
+
constructor(config, repoDir) {
|
|
179
|
+
this.config = config;
|
|
180
|
+
this.repoDir = repoDir;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Environment for an agent launch: the backend's own env plus the issue
|
|
184
|
+
* file (body and the human thread), the report contract (schema + report
|
|
185
|
+
* file, with any previous launch's report cleared), and the turn cap.
|
|
186
|
+
*/
|
|
187
|
+
async launchEnv(brief, backendEnv = {}, extra = {}) {
|
|
188
|
+
const stateDir = state.getTaskStateDir(this.config.repo, brief.issue);
|
|
189
|
+
let comments = [];
|
|
190
|
+
try {
|
|
191
|
+
comments = await (0, thread_js_1.collectThread)(this.config, brief.issue);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
log.warn(`Could not read the comments on #${brief.issue}: ${err.message}. The agent gets the body only.`);
|
|
195
|
+
}
|
|
196
|
+
const issueFile = (0, issue_file_js_1.writeIssueFile)(stateDir, {
|
|
197
|
+
number: brief.issue,
|
|
198
|
+
title: brief.title,
|
|
199
|
+
body: brief.body,
|
|
200
|
+
url: `https://github.com/${this.config.repo}/issues/${brief.issue}`,
|
|
201
|
+
comments,
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
...backendEnv,
|
|
205
|
+
...extra,
|
|
206
|
+
...issueFile,
|
|
207
|
+
FOUNDRY_MAX_TURNS: String(this.config.agent_max_turns ?? 150),
|
|
208
|
+
...(0, agent_report_js_1.prepareLaunch)(stateDir),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
briefFor(task) {
|
|
212
|
+
return { issue: task.issue, title: task.title, body: task.issue_body ?? '' };
|
|
213
|
+
}
|
|
214
|
+
async handleEvent(event) {
|
|
215
|
+
switch (event.type) {
|
|
216
|
+
case 'command':
|
|
217
|
+
return this.handleCommand(event);
|
|
218
|
+
case 'pr_merged':
|
|
219
|
+
return this.handlePRMerged(event);
|
|
220
|
+
case 'pr_review':
|
|
221
|
+
return this.handlePRReview(event);
|
|
222
|
+
case 'agent_completed':
|
|
223
|
+
return this.handleAgentCompleted(event);
|
|
224
|
+
case 'pr_preview':
|
|
225
|
+
return this.handlePRPreview(event);
|
|
226
|
+
case 'pr_approved':
|
|
227
|
+
return this.handlePRApproved(event);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ── Approval → rebase, checks, merge ──────────────────────────────────
|
|
231
|
+
async handlePRApproved(event) {
|
|
232
|
+
const config = this.config;
|
|
233
|
+
const label = `PR #${event.pr_number} (${event.branch} → ${event.base})`;
|
|
234
|
+
log.info(`${label} approved by ${event.reviewer}. Reviewing...`);
|
|
235
|
+
const outcome = await review.reviewAndMerge(config, this.repoDir, {
|
|
236
|
+
pr_number: event.pr_number, branch: event.branch, base: event.base, reviewer: event.reviewer,
|
|
237
|
+
});
|
|
238
|
+
if (!outcome.merged) {
|
|
239
|
+
log.warn(`${label} not merged: ${outcome.reason} (${outcome.detail}).`);
|
|
240
|
+
// A conflict is work for the agent, when there is one.
|
|
241
|
+
if ((outcome.reason === 'conflicting' || outcome.reason === 'rebase_failed') && event.task) {
|
|
242
|
+
const instruction = stalePRs.conflictInstruction(event.base, 0).replace(' (PR #0 merged)', '');
|
|
243
|
+
const dispatched = stalePRs.dispatchCommand(event.task.issue, 'go', instruction);
|
|
244
|
+
const note = dispatched
|
|
245
|
+
? `PR #${event.pr_number} was approved but conflicts with \`${event.base}\`. Foundry has sent the agent to rebase and resolve; approve again once it is clean.`
|
|
246
|
+
: `PR #${event.pr_number} was approved but conflicts with \`${event.base}\`. Comment \`@foundry go\` with:\n\n> ${instruction}`;
|
|
247
|
+
try {
|
|
248
|
+
await github.addComment(config.repo, event.task.issue, note);
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
}
|
|
252
|
+
// So is a verify failure on the rebased head: the branch was pushed
|
|
253
|
+
// rebased, and the check run on the PR says what broke.
|
|
254
|
+
if (outcome.reason === 'verify_failed' && event.task) {
|
|
255
|
+
const instruction = `The branch was rebased onto \`${event.base}\` and verification fails on it; see the Foundry Verification check on PR #${event.pr_number}. Fix the failure on the branch (do not start over) and push.`;
|
|
256
|
+
const dispatched = stalePRs.dispatchCommand(event.task.issue, 'go', instruction);
|
|
257
|
+
const note = dispatched
|
|
258
|
+
? `PR #${event.pr_number} was approved, but verification failed after the rebase onto \`${event.base}\`. Foundry has sent the agent to fix it; approve again once the PR is green.`
|
|
259
|
+
: `PR #${event.pr_number} was approved, but verification failed after the rebase onto \`${event.base}\`. Comment \`@foundry go\` with:\n\n> ${instruction}`;
|
|
260
|
+
try {
|
|
261
|
+
await github.addComment(config.repo, event.task.issue, note);
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Merged in this job: the pull_request.closed event may never arrive
|
|
268
|
+
// (GITHUB_TOKEN), so complete the lifecycle here. Both paths are
|
|
269
|
+
// idempotent if it does arrive.
|
|
270
|
+
if (event.task) {
|
|
271
|
+
await this.handlePRMerged({ type: 'pr_merged', task: event.task, base: event.base });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
await this.handlePRPreview({ type: 'pr_preview', action: 'down', pr_number: event.pr_number, branch: event.branch, base: event.base, sha: outcome.sha });
|
|
275
|
+
await stalePRs.handleStalePRs(config, event.base, event.pr_number);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ── Previews for pull requests that are not tasks ─────────────────────
|
|
279
|
+
async handlePRPreview(event) {
|
|
280
|
+
const config = this.config;
|
|
281
|
+
const label = `PR #${event.pr_number} (${event.branch} → ${event.base})`;
|
|
282
|
+
if (event.action === 'up') {
|
|
283
|
+
log.info(`Preview up for ${label}`);
|
|
284
|
+
try {
|
|
285
|
+
const result = await preview.previewUpTarget(config, {
|
|
286
|
+
branch: event.branch,
|
|
287
|
+
pr_number: event.pr_number,
|
|
288
|
+
sha: event.sha,
|
|
289
|
+
});
|
|
290
|
+
if (!result) {
|
|
291
|
+
log.warn(`No preview URL resolved for ${label}.`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (previewVerify.isConfigured(config)) {
|
|
295
|
+
await this.verifyPullRequestPreview(event, result.url);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
log.warn(`Preview up failed for ${label}: ${err.message}`);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const url = await preview.findPreviewUrlOnPR(config.repo, event.pr_number);
|
|
304
|
+
if (!url) {
|
|
305
|
+
log.info(`No preview recorded on ${label}; nothing to tear down.`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
log.info(`Preview down for ${label}`);
|
|
309
|
+
try {
|
|
310
|
+
await preview.previewDownTarget(config, {
|
|
311
|
+
branch: event.branch,
|
|
312
|
+
pr_number: event.pr_number,
|
|
313
|
+
sha: event.sha,
|
|
314
|
+
preview_url: url,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
log.warn(`Preview down failed for ${label}: ${err.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Run `verify_preview` for a non-task PR from a throwaway checkout of its
|
|
323
|
+
* head branch. The result goes on the PR as a comment and, when the token
|
|
324
|
+
* allows, as a check run on the head SHA.
|
|
325
|
+
*/
|
|
326
|
+
async verifyPullRequestPreview(event, url) {
|
|
327
|
+
const config = this.config;
|
|
328
|
+
const label = `PR #${event.pr_number}`;
|
|
329
|
+
const cwd = path.resolve(this.repoDir, config.worktree_base, `pr-${event.pr_number}`);
|
|
330
|
+
try {
|
|
331
|
+
try {
|
|
332
|
+
git.fetchAll(this.repoDir);
|
|
333
|
+
}
|
|
334
|
+
catch { }
|
|
335
|
+
git.pruneWorktrees(this.repoDir);
|
|
336
|
+
if (git.worktreeExists(cwd, this.repoDir))
|
|
337
|
+
git.removeWorktree(cwd, this.repoDir);
|
|
338
|
+
git.addWorktreeTracking(cwd, event.branch, this.repoDir);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
log.warn(`Could not check out ${event.branch} for preview verification of ${label}: ${err.message}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
let outcome;
|
|
345
|
+
try {
|
|
346
|
+
log.info(`Running preview verification for ${label} against ${url}...`);
|
|
347
|
+
outcome = await previewVerify.verifyPreview(config, {
|
|
348
|
+
url,
|
|
349
|
+
cwd,
|
|
350
|
+
env: { FOUNDRY_BRANCH: event.branch, FOUNDRY_PR_NUMBER: String(event.pr_number) },
|
|
351
|
+
sha: event.sha, branch: event.branch, pr_number: event.pr_number,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
try {
|
|
356
|
+
git.removeWorktree(cwd, this.repoDir);
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
}
|
|
360
|
+
(outcome.passed ? log.success : log.error)(`Preview verification ${outcome.passed ? 'passed' : 'failed'} for ${label}.`);
|
|
361
|
+
const body = PREVIEW_VERIFY_MARKER + '\n' + previewVerify.formatPreviewVerify(outcome);
|
|
362
|
+
try {
|
|
363
|
+
const comments = await github.getComments(config.repo, event.pr_number);
|
|
364
|
+
const existing = comments.find(c => c.body.includes(PREVIEW_VERIFY_MARKER));
|
|
365
|
+
if (existing)
|
|
366
|
+
await github.updateComment(config.repo, existing.id, body);
|
|
367
|
+
else
|
|
368
|
+
await github.commentOnPR(config.repo, event.pr_number, body);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
log.warn(`Could not post preview verification on ${label}: ${err.message}`);
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await github.createCheckRun(config.repo, {
|
|
375
|
+
name: 'Foundry Preview Verification',
|
|
376
|
+
head_sha: event.sha,
|
|
377
|
+
status: 'completed',
|
|
378
|
+
conclusion: outcome.passed ? 'success' : 'failure',
|
|
379
|
+
output: {
|
|
380
|
+
title: outcome.passed ? 'Preview verification passed' : 'Preview verification failed',
|
|
381
|
+
summary: previewVerify.formatPreviewVerify(outcome),
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
log.warn(`Failed to create check run for ${label}: ${err.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// ── Commands ─────────────────────────────────────────────────────────
|
|
390
|
+
async handleCommand(event) {
|
|
391
|
+
switch (event.command) {
|
|
392
|
+
case 'go':
|
|
393
|
+
return this.handleCommandGo(event);
|
|
394
|
+
case 'restart':
|
|
395
|
+
return this.handleCommandRestart(event);
|
|
396
|
+
case 'stop':
|
|
397
|
+
return this.handleCommandStop(event);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* `@foundry go`: start the issue if it has never been started; otherwise
|
|
402
|
+
* proceed from the branch. The agent reads the whole thread from the issue
|
|
403
|
+
* file, so the same command answers a question, approves a plan, revises a
|
|
404
|
+
* PR, or picks up a failed task. On a task whose label still says running,
|
|
405
|
+
* the job that ran it is gone (a live job would have made this one wait),
|
|
406
|
+
* so it is resumed like any other.
|
|
407
|
+
*/
|
|
408
|
+
async handleCommandGo(event) {
|
|
409
|
+
const config = this.config;
|
|
410
|
+
const { task } = event;
|
|
411
|
+
if (!task || task.status === 'claimed' || task.status === 'done') {
|
|
412
|
+
const issue = await github.getIssue(config.repo, event.issue);
|
|
413
|
+
await this.startTask(issue);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const message = event.message ?? '';
|
|
417
|
+
const kind = task.status === 'waiting-for-input' ? 'answer' :
|
|
418
|
+
task.status === 'plan-review' ? 'plan' :
|
|
419
|
+
'go';
|
|
420
|
+
await this.relaunch(task, message, kind);
|
|
421
|
+
const fresh = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'agent-running' };
|
|
422
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
423
|
+
task: fresh,
|
|
424
|
+
message: 'Agent is working per `@foundry go`. `@foundry stop` to stop it.',
|
|
425
|
+
historyEntry: 'Resumed via @foundry go',
|
|
426
|
+
});
|
|
427
|
+
log.success(`Resumed #${task.issue} via @foundry go (${kind})`);
|
|
428
|
+
}
|
|
429
|
+
/** `@foundry restart`: discard everything and start the issue again, in this job. */
|
|
430
|
+
async handleCommandRestart(event) {
|
|
431
|
+
const config = this.config;
|
|
432
|
+
const { task } = event;
|
|
433
|
+
if (task) {
|
|
434
|
+
if (mayBeRunning(task)) {
|
|
435
|
+
const worker = getWorker(config);
|
|
436
|
+
await worker.handleFor(task.tmux_session).kill();
|
|
437
|
+
}
|
|
438
|
+
await triggerPreviewDown(config, task);
|
|
439
|
+
state.removeTask(config.repo, task.issue);
|
|
440
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
441
|
+
task: { ...task, status: 'stopped' },
|
|
442
|
+
message: 'Work discarded per `@foundry restart`. Starting over.',
|
|
443
|
+
historyEntry: 'Restarted via @foundry restart',
|
|
444
|
+
});
|
|
445
|
+
log.info(`Discarded work on #${task.issue} via @foundry restart`);
|
|
446
|
+
}
|
|
447
|
+
// startTask removes the old worktree, branch, and remote branch itself.
|
|
448
|
+
const issue = await github.getIssue(config.repo, event.issue);
|
|
449
|
+
await this.startTask(issue);
|
|
450
|
+
}
|
|
451
|
+
/** `@foundry stop`: stop the agent, keep the branch, mark the task failed. */
|
|
452
|
+
async handleCommandStop(event) {
|
|
453
|
+
const config = this.config;
|
|
454
|
+
const { task } = event;
|
|
455
|
+
if (!task) {
|
|
456
|
+
log.info(`#${event.issue} has no task to stop.`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (mayBeRunning(task)) {
|
|
460
|
+
const worker = getWorker(config);
|
|
461
|
+
await worker.handleFor(task.tmux_session).kill();
|
|
462
|
+
}
|
|
463
|
+
const preserved = (0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, 'stopped');
|
|
464
|
+
await triggerPreviewDown(config, task);
|
|
465
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed');
|
|
466
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'failed');
|
|
467
|
+
const stoppedTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
468
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
469
|
+
task: stoppedTask,
|
|
470
|
+
message: ['Agent stopped per `@foundry stop`.', ...workOnBranch(task, preserved), '', exports.FAILED_FOOTER].join('\n'),
|
|
471
|
+
historyEntry: 'Stopped via @foundry stop',
|
|
472
|
+
});
|
|
473
|
+
log.success(`Stopped agent for #${task.issue} via @foundry stop`);
|
|
474
|
+
}
|
|
475
|
+
// ── Fresh start (claim + worktree + agent) ───────────────────────────
|
|
476
|
+
async startTask(issue) {
|
|
477
|
+
const config = this.config;
|
|
478
|
+
const repoDir = this.repoDir;
|
|
479
|
+
const runnerId = state.getRunnerId();
|
|
480
|
+
const branch = git.resolveBranchName(config.branch_template, issue.number, issue.title);
|
|
481
|
+
const worktree = git.resolveWorktreePath(config.worktree_base, issue.number, issue.title, repoDir);
|
|
482
|
+
const workerId = git.workerId(issue.number);
|
|
483
|
+
const issueLabels = issue.labels.map(l => l.name);
|
|
484
|
+
const backend = (0, index_js_2.resolveBackendForIssue)(config, issueLabels);
|
|
485
|
+
try {
|
|
486
|
+
git.fetchAll(repoDir);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
log.warn(`Fetch failed: ${err.message}`);
|
|
490
|
+
}
|
|
491
|
+
// The base must exist before anything is claimed: a misnamed integration
|
|
492
|
+
// branch is a configuration error, reported on the issue, not a fallback.
|
|
493
|
+
let base;
|
|
494
|
+
try {
|
|
495
|
+
base = (0, branches_js_1.resolveBaseBranch)(config, repoDir);
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
log.error(err.message);
|
|
499
|
+
try {
|
|
500
|
+
await github.addComment(config.repo, issue.number, `**Foundry cannot start.** ${err.message}`);
|
|
501
|
+
}
|
|
502
|
+
catch { }
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
log.info(`Claiming #${issue.number}: ${issue.title}`);
|
|
506
|
+
const claimed = await claim.claimIssue(config, issue, {
|
|
507
|
+
runner_id: runnerId,
|
|
508
|
+
branch,
|
|
509
|
+
worktree,
|
|
510
|
+
tmux_session: workerId,
|
|
511
|
+
agent_backend: backend.name,
|
|
512
|
+
});
|
|
513
|
+
if (!claimed) {
|
|
514
|
+
log.warn(`Failed to claim #${issue.number} — another runner may own it.`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
log.success(`Claimed #${issue.number}`);
|
|
518
|
+
if (git.worktreeExists(worktree, repoDir)) {
|
|
519
|
+
log.info(`Removing stale worktree from previous attempt: ${worktree}`);
|
|
520
|
+
git.removeWorktree(worktree, repoDir);
|
|
521
|
+
}
|
|
522
|
+
if (git.branchExists(branch, repoDir)) {
|
|
523
|
+
log.info(`Removing stale branch from previous attempt: ${branch}`);
|
|
524
|
+
git.deleteBranch(branch, repoDir);
|
|
525
|
+
}
|
|
526
|
+
if (git.remoteBranchExists(branch, repoDir)) {
|
|
527
|
+
log.info(`Removing stale remote branch from previous attempt: ${branch}`);
|
|
528
|
+
git.deleteRemoteBranch(branch, repoDir);
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
git.addWorktree(worktree, branch, base, repoDir);
|
|
532
|
+
log.success(`Worktree: ${worktree}`);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
log.error(`Failed to create worktree: ${err.message}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const logDir = state.getLogDir(config.repo, issue.number);
|
|
539
|
+
const stateDir = state.getTaskStateDir(config.repo, issue.number);
|
|
540
|
+
const permissionMode = resolvePermissionMode(issueLabels, config);
|
|
541
|
+
const params = {
|
|
542
|
+
worktree,
|
|
543
|
+
issue_url: issue.html_url,
|
|
544
|
+
issue_number: issue.number,
|
|
545
|
+
repo: config.repo,
|
|
546
|
+
title: issue.title,
|
|
547
|
+
body: issue.body ?? '',
|
|
548
|
+
labels: issueLabels,
|
|
549
|
+
log_dir: logDir,
|
|
550
|
+
state_dir: stateDir,
|
|
551
|
+
permission_mode: permissionMode,
|
|
552
|
+
};
|
|
553
|
+
const agentCommand = backend.resolveCommand(params);
|
|
554
|
+
const agentEnv = backend.resolveEnv(params);
|
|
555
|
+
const worker = getWorker(config);
|
|
556
|
+
await worker.spawn({
|
|
557
|
+
id: workerId,
|
|
558
|
+
cwd: worktree,
|
|
559
|
+
command: agentCommand,
|
|
560
|
+
env: await this.launchEnv({ issue: issue.number, title: issue.title, body: issue.body ?? '' }, agentEnv),
|
|
561
|
+
});
|
|
562
|
+
const taskState = {
|
|
563
|
+
issue: issue.number,
|
|
564
|
+
title: issue.title,
|
|
565
|
+
issue_body: issue.body ?? '',
|
|
566
|
+
repo: config.repo,
|
|
567
|
+
branch,
|
|
568
|
+
worktree,
|
|
569
|
+
tmux_session: workerId,
|
|
570
|
+
agent_backend: backend.name,
|
|
571
|
+
agent_command: agentCommand,
|
|
572
|
+
permission_mode: permissionMode,
|
|
573
|
+
status: 'agent-running',
|
|
574
|
+
claimed_at: new Date().toISOString(),
|
|
575
|
+
updated_at: new Date().toISOString(),
|
|
576
|
+
runner_id: runnerId,
|
|
577
|
+
};
|
|
578
|
+
state.upsertTask(config.repo, taskState);
|
|
579
|
+
await statusComment.updateStatusComment(config.repo, issue.number, {
|
|
580
|
+
task: taskState,
|
|
581
|
+
message: 'Agent is working on this issue. `@foundry stop` to stop it.',
|
|
582
|
+
historyEntry: 'Agent started',
|
|
583
|
+
});
|
|
584
|
+
log.success(`Agent launched in ${workerId}`);
|
|
585
|
+
}
|
|
586
|
+
// ── PR Merged ────────────────────────────────────────────────────────
|
|
587
|
+
async handlePRMerged(event) {
|
|
588
|
+
const { task } = event;
|
|
589
|
+
const config = this.config;
|
|
590
|
+
const repoDir = this.repoDir;
|
|
591
|
+
log.success(`PR for #${task.issue} has been merged. Completing task.`);
|
|
592
|
+
await triggerPreviewDown(config, task);
|
|
593
|
+
try {
|
|
594
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'done');
|
|
595
|
+
}
|
|
596
|
+
catch { }
|
|
597
|
+
try {
|
|
598
|
+
await github.closeIssue(config.repo, task.issue);
|
|
599
|
+
}
|
|
600
|
+
catch { }
|
|
601
|
+
try {
|
|
602
|
+
git.removeWorktree(task.worktree, repoDir);
|
|
603
|
+
}
|
|
604
|
+
catch { }
|
|
605
|
+
try {
|
|
606
|
+
git.deleteBranch(task.branch, repoDir);
|
|
607
|
+
}
|
|
608
|
+
catch { }
|
|
609
|
+
state.removeTask(config.repo, task.issue);
|
|
610
|
+
log.success(`Auto-cleaned resources for #${task.issue}.`);
|
|
611
|
+
// integration moved: any sibling PR that now conflicts gets its agent back.
|
|
612
|
+
await stalePRs.handleStalePRs(config, event.base ?? (0, branches_js_1.integrationBranch)(config), task.pr_number);
|
|
613
|
+
}
|
|
614
|
+
// ── PR Review ────────────────────────────────────────────────────────
|
|
615
|
+
async handlePRReview(event) {
|
|
616
|
+
const { task, feedback } = event;
|
|
617
|
+
const config = this.config;
|
|
618
|
+
log.info(`PR review feedback found for #${task.issue}. Resuming agent...`);
|
|
619
|
+
state.updateTaskStatus(config.repo, task.issue, 'pr-changes-requested', {
|
|
620
|
+
last_agent_message: feedback,
|
|
621
|
+
});
|
|
622
|
+
await this.resumeAgentForPR(task, feedback);
|
|
623
|
+
}
|
|
624
|
+
// ── Agent Completed ──────────────────────────────────────────────────
|
|
625
|
+
async handleAgentCompleted(event) {
|
|
626
|
+
const { task, outcome } = event;
|
|
627
|
+
const config = this.config;
|
|
628
|
+
log.info(`Outcome for #${task.issue}: ${outcome.type}`);
|
|
629
|
+
if (outcome.session_id || outcome.usage) {
|
|
630
|
+
state.updateTaskStatus(config.repo, task.issue, task.status, {
|
|
631
|
+
...(outcome.session_id ? { session_id: outcome.session_id } : {}),
|
|
632
|
+
...(outcome.usage ? { last_run: outcome.usage } : {}),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
switch (outcome.type) {
|
|
636
|
+
case 'completed':
|
|
637
|
+
await this.handleCompleted(task, outcome);
|
|
638
|
+
break;
|
|
639
|
+
case 'plan-completed':
|
|
640
|
+
await this.handlePlanCompleted(task, outcome);
|
|
641
|
+
break;
|
|
642
|
+
case 'needs-input':
|
|
643
|
+
await this.handleNeedsInput(task, outcome);
|
|
644
|
+
break;
|
|
645
|
+
case 'blocked':
|
|
646
|
+
case 'errored':
|
|
647
|
+
await this.handleFailedRun(task, outcome);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/** The agent ended without finishing. The branch is kept so `go` can continue it. */
|
|
652
|
+
async handleFailedRun(task, outcome) {
|
|
653
|
+
const config = this.config;
|
|
654
|
+
log.error(`Agent for #${task.issue} ${outcome.type}: ${outcome.reason ?? 'no reason given'}`);
|
|
655
|
+
const preserved = (0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, outcome.type);
|
|
656
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed', {
|
|
657
|
+
last_agent_message: outcome.final_message ?? undefined,
|
|
658
|
+
});
|
|
659
|
+
await claim.markFailed(config, task.issue);
|
|
660
|
+
const errorMsg = [
|
|
661
|
+
outcome.final_message ? outcome.final_message.slice(-2000) : 'No error details captured.',
|
|
662
|
+
...workOnBranch(task, preserved),
|
|
663
|
+
'',
|
|
664
|
+
exports.FAILED_FOOTER,
|
|
665
|
+
].join('\n');
|
|
666
|
+
const freshTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
667
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
668
|
+
task: freshTask,
|
|
669
|
+
message: errorMsg,
|
|
670
|
+
historyEntry: outcome.type === 'blocked' ? 'Agent blocked' : 'Agent errored',
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
// ── Completed agent sub-handlers ─────────────────────────────────────
|
|
674
|
+
/**
|
|
675
|
+
* The last mile after a PR exists: deploy the preview, verify against it
|
|
676
|
+
* when `verify_preview` is configured, and only then mark the task ready
|
|
677
|
+
* for human review. A failed preview verification fails the task with the
|
|
678
|
+
* output on the issue, exactly like a failed build.
|
|
679
|
+
*/
|
|
680
|
+
async finishReadyForReview(task, message, historyEntry, prUrl) {
|
|
681
|
+
const config = this.config;
|
|
682
|
+
const fresh = () => state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? task;
|
|
683
|
+
await triggerPreviewUp(config, task);
|
|
684
|
+
let verified = null;
|
|
685
|
+
if (previewVerify.isConfigured(config)) {
|
|
686
|
+
const current = fresh();
|
|
687
|
+
if (!current.preview_url) {
|
|
688
|
+
verified = {
|
|
689
|
+
passed: false,
|
|
690
|
+
url: '(none)',
|
|
691
|
+
results: [{ command: 'preview', success: false, output: 'verify_preview is configured but no preview URL was produced. Check the `preview` section of .joynt-foundry.yml.', duration_ms: 0 }],
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
log.info(`Running preview verification for #${task.issue} against ${current.preview_url}...`);
|
|
696
|
+
verified = await previewVerify.verifyPreview(config, {
|
|
697
|
+
url: current.preview_url,
|
|
698
|
+
cwd: current.worktree,
|
|
699
|
+
env: {
|
|
700
|
+
FOUNDRY_ISSUE: String(task.issue),
|
|
701
|
+
FOUNDRY_BRANCH: current.branch,
|
|
702
|
+
FOUNDRY_PR_NUMBER: String(current.pr_number ?? ''),
|
|
703
|
+
},
|
|
704
|
+
sha: git.headSha(current.worktree), branch: current.branch, pr_number: current.pr_number,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
if (!verified.passed) {
|
|
708
|
+
log.error(`Preview verification failed for #${task.issue}.`);
|
|
709
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed');
|
|
710
|
+
await claim.markFailed(config, task.issue);
|
|
711
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
712
|
+
task: { ...fresh(), status: 'failed' },
|
|
713
|
+
prUrl,
|
|
714
|
+
message: previewVerify.formatPreviewVerify(verified) + '\n\n' + exports.FAILED_FOOTER,
|
|
715
|
+
historyEntry: 'Preview verification failed',
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
log.success(`Preview verification passed for #${task.issue}.`);
|
|
720
|
+
}
|
|
721
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'ready_for_review');
|
|
722
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
723
|
+
task: { ...fresh(), status: 'pr-open' },
|
|
724
|
+
prUrl,
|
|
725
|
+
message: [
|
|
726
|
+
message,
|
|
727
|
+
verified ? `Preview verification passed (${verified.results.length} check${verified.results.length === 1 ? '' : 's'}).` : null,
|
|
728
|
+
'',
|
|
729
|
+
'Foundry is watching this PR. Submit a review with **Request changes**, or comment `@foundry go <feedback>` on this issue.',
|
|
730
|
+
].filter(l => l !== null).join('\n'),
|
|
731
|
+
historyEntry: verified ? `${historyEntry}, preview verified` : historyEntry,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
async handleCompleted(task, outcome) {
|
|
735
|
+
const config = this.config;
|
|
736
|
+
log.info(`Running verification for #${task.issue}...`);
|
|
737
|
+
state.updateTaskStatus(config.repo, task.issue, 'verifying');
|
|
738
|
+
// The check run is created after the branch is pushed, never before: GitHub
|
|
739
|
+
// refuses a check run for a commit it has not seen ("No commit found for SHA").
|
|
740
|
+
const headSha = git.headSha(task.worktree);
|
|
741
|
+
const reportCheck = async (conclusion, output) => {
|
|
742
|
+
try {
|
|
743
|
+
await github.createCheckRun(config.repo, { name: verify.VERIFY_CHECK_NAME, head_sha: headSha, status: 'completed', conclusion, output });
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
log.warn(`Failed to create check run for #${task.issue}: ${err.message}`);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
const result = verify.runVerifyPipeline(config, task.worktree);
|
|
750
|
+
const stepSummary = result.results.map(r => `- \`${r.command}\`: ${r.success ? 'passed' : 'FAILED'} (${r.duration_ms}ms)`).join('\n');
|
|
751
|
+
if (result.passed) {
|
|
752
|
+
log.success(`Verification passed for #${task.issue}.`);
|
|
753
|
+
try {
|
|
754
|
+
// --force-with-lease: a relaunched agent may have rebased. The lease is
|
|
755
|
+
// the remote-tracking ref this job fetched, so a push by anyone else
|
|
756
|
+
// since then is still refused.
|
|
757
|
+
git.push(task.branch, task.worktree, true);
|
|
758
|
+
log.success(`Pushed ${task.branch}`);
|
|
759
|
+
await reportCheck('success', { title: 'Verification passed', summary: stepSummary });
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
log.error(`Push failed for #${task.issue}: ${err.message}`);
|
|
763
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed');
|
|
764
|
+
await claim.markFailed(config, task.issue);
|
|
765
|
+
const pushFailedTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
766
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
767
|
+
task: pushFailedTask,
|
|
768
|
+
message: `Push failed:\n\n${err.message}\n\n${exports.FAILED_FOOTER}`,
|
|
769
|
+
historyEntry: 'Push failed',
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (task.pr_url) {
|
|
774
|
+
log.success(`PR already exists for #${task.issue}: ${task.pr_url}`);
|
|
775
|
+
// The description is the reviewer's summary of what the branch now
|
|
776
|
+
// holds; after a resumed run it must describe this run, not the first.
|
|
777
|
+
const prNumber = task.pr_number ?? github.extractPRNumber(task.pr_url);
|
|
778
|
+
if (prNumber) {
|
|
779
|
+
try {
|
|
780
|
+
await github.updatePRBody(config.repo, prNumber, buildPRBody(task, outcome));
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
log.warn(`Could not update the PR description for #${task.issue}: ${err.message}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
state.updateTaskStatus(config.repo, task.issue, 'pr-open');
|
|
787
|
+
await this.finishReadyForReview(task, 'Verification passed. PR updated with new changes.', 'Verification passed, PR updated');
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
try {
|
|
791
|
+
const prUrl = await github.createPR(config.repo, {
|
|
792
|
+
// Issues and PRs share one number sequence, so a leading "#<issue>:"
|
|
793
|
+
// reads as the PR's own number, off by one. Reference the issue at the end.
|
|
794
|
+
title: `${task.title} (issue #${task.issue})`,
|
|
795
|
+
body: buildPRBody(task, outcome),
|
|
796
|
+
head: task.branch,
|
|
797
|
+
base: (0, branches_js_1.integrationBranch)(config),
|
|
798
|
+
});
|
|
799
|
+
log.success(`PR created: ${prUrl}`);
|
|
800
|
+
const prNumber = github.extractPRNumber(prUrl);
|
|
801
|
+
state.updateTaskStatus(config.repo, task.issue, 'pr-open', {
|
|
802
|
+
pr_url: prUrl,
|
|
803
|
+
pr_number: prNumber ?? undefined,
|
|
804
|
+
});
|
|
805
|
+
await this.finishReadyForReview(task, 'Verification passed. PR created and ready for review.', 'Verification passed, PR created', prUrl);
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
log.error(`PR creation failed for #${task.issue}: ${err.message}`);
|
|
809
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed');
|
|
810
|
+
await claim.markFailed(config, task.issue);
|
|
811
|
+
const prFailTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
812
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
813
|
+
task: prFailTask,
|
|
814
|
+
message: `PR creation failed:\n\n${err.message}\n\n${exports.FAILED_FOOTER}`,
|
|
815
|
+
historyEntry: 'PR creation failed',
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
log.error(`Verification failed for #${task.issue}.`);
|
|
822
|
+
for (const r of result.results) {
|
|
823
|
+
if (!r.success) {
|
|
824
|
+
log.error(` Failed: ${r.command}`);
|
|
825
|
+
log.error(` ${r.output.slice(-500)}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// The branch is pushed so `@foundry go` can send the agent back to it.
|
|
829
|
+
const preserved = (0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, 'verification failed');
|
|
830
|
+
if (preserved) {
|
|
831
|
+
const annotations = verify.parseAnnotations(result.results);
|
|
832
|
+
await reportCheck('failure', {
|
|
833
|
+
title: 'Verification failed',
|
|
834
|
+
summary: stepSummary,
|
|
835
|
+
annotations: annotations.length > 0 ? annotations : undefined,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed');
|
|
839
|
+
await claim.markFailed(config, task.issue);
|
|
840
|
+
const failureMsg = [
|
|
841
|
+
...result.results.map(r => `- \`${r.command}\`: ${r.success ? 'passed' : 'FAILED'} (${r.duration_ms}ms)`),
|
|
842
|
+
'',
|
|
843
|
+
result.results
|
|
844
|
+
.filter(r => !r.success)
|
|
845
|
+
.map(r => `\`\`\`\n${r.output.slice(-1000)}\n\`\`\``)
|
|
846
|
+
.join('\n'),
|
|
847
|
+
...workOnBranch(task, preserved),
|
|
848
|
+
'',
|
|
849
|
+
exports.FAILED_FOOTER,
|
|
850
|
+
].join('\n');
|
|
851
|
+
const verifyFailTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
852
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
853
|
+
task: verifyFailTask,
|
|
854
|
+
message: failureMsg,
|
|
855
|
+
historyEntry: 'Verification failed',
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async handleNeedsInput(task, outcome) {
|
|
860
|
+
const config = this.config;
|
|
861
|
+
const inputRound = (task.input_request_count ?? 0) + 1;
|
|
862
|
+
if (inputRound > config.max_input_rounds) {
|
|
863
|
+
log.error(`#${task.issue} exceeded max input rounds (${config.max_input_rounds}). Marking failed.`);
|
|
864
|
+
const preserved = (0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, 'too many questions');
|
|
865
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed', {
|
|
866
|
+
input_request_count: inputRound,
|
|
867
|
+
last_agent_message: outcome.final_message ?? undefined,
|
|
868
|
+
});
|
|
869
|
+
await claim.markFailed(config, task.issue);
|
|
870
|
+
const giveUpTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
871
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
872
|
+
task: giveUpTask,
|
|
873
|
+
message: [`Exceeded ${config.max_input_rounds} input rounds. The agent has been unable to complete this task.`, ...workOnBranch(task, preserved), '', exports.FAILED_FOOTER].join('\n'),
|
|
874
|
+
historyEntry: `Gave up after ${config.max_input_rounds} input rounds`,
|
|
875
|
+
});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
log.info(`#${task.issue} needs input (round ${inputRound}).`);
|
|
879
|
+
const preserved = (0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, 'waiting for input');
|
|
880
|
+
try {
|
|
881
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'waiting_for_input');
|
|
882
|
+
}
|
|
883
|
+
catch { }
|
|
884
|
+
state.updateTaskStatus(config.repo, task.issue, 'waiting-for-input', {
|
|
885
|
+
session_id: outcome.session_id ?? task.session_id,
|
|
886
|
+
input_request_count: inputRound,
|
|
887
|
+
last_agent_message: outcome.final_message ?? undefined,
|
|
888
|
+
});
|
|
889
|
+
const inputComment = agentOutput.formatInputRequestComment(outcome, inputRound);
|
|
890
|
+
const inputMsg = inputComment.replace('<!-- foundry-input-request -->\n', '')
|
|
891
|
+
+ (preserved.pushed ? `\n\n_Work so far is on \`${task.branch}\`._` : '');
|
|
892
|
+
const waitingTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'waiting-for-input' };
|
|
893
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
894
|
+
task: waitingTask,
|
|
895
|
+
message: inputMsg,
|
|
896
|
+
historyEntry: `Input needed (round ${inputRound})`,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
async handlePlanCompleted(task, outcome) {
|
|
900
|
+
const config = this.config;
|
|
901
|
+
(0, worktree_js_1.preserveWork)(this.config, task, this.repoDir, 'plan review');
|
|
902
|
+
// Post the plan-review comment separately — it has its own marker.
|
|
903
|
+
const planComment = [
|
|
904
|
+
'<!-- foundry-plan-review -->',
|
|
905
|
+
'**Foundry Agent — Plan for Review**',
|
|
906
|
+
'',
|
|
907
|
+
outcome.final_message ?? 'No plan content captured.',
|
|
908
|
+
'',
|
|
909
|
+
'---',
|
|
910
|
+
'Comment `@foundry go` to approve the plan, or `@foundry go <feedback>` to have it revised.',
|
|
911
|
+
'',
|
|
912
|
+
'Or: `@foundry restart` · `@foundry stop`',
|
|
913
|
+
].join('\n');
|
|
914
|
+
await postToConversationTarget(config, task, planComment);
|
|
915
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'plan_review');
|
|
916
|
+
state.updateTaskStatus(config.repo, task.issue, 'plan-review', {
|
|
917
|
+
session_id: outcome.session_id ?? task.session_id,
|
|
918
|
+
last_agent_message: outcome.final_message ?? undefined,
|
|
919
|
+
});
|
|
920
|
+
const planTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'plan-review' };
|
|
921
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
922
|
+
task: planTask,
|
|
923
|
+
message: 'Plan ready for review. `@foundry go` approves it; `@foundry go <feedback>` revises it.',
|
|
924
|
+
historyEntry: 'Plan completed, awaiting review',
|
|
925
|
+
});
|
|
926
|
+
log.success(`Plan posted for #${task.issue}. Awaiting human review.`);
|
|
927
|
+
}
|
|
928
|
+
// ── Relaunch ─────────────────────────────────────────────────────────
|
|
929
|
+
/**
|
|
930
|
+
* Launch the agent again on the task's branch. Sessions do not survive
|
|
931
|
+
* jobs, so this is a fresh process: the worktree is recreated from the
|
|
932
|
+
* pushed branch, the thread goes in the issue file, and why it is being
|
|
933
|
+
* launched goes in FOUNDRY_RESUME_CONTEXT. Always in auto mode: a plan has
|
|
934
|
+
* been approved or a human has asked for work.
|
|
935
|
+
*/
|
|
936
|
+
async relaunch(task, humanText, kind) {
|
|
937
|
+
const config = this.config;
|
|
938
|
+
(0, worktree_js_1.ensureWorktree)(this.config, task, this.repoDir);
|
|
939
|
+
await (0, labels_js_1.setStateLabel)(config, task.issue, 'in_progress');
|
|
940
|
+
const worker = getWorker(config);
|
|
941
|
+
await worker.handleFor(task.tmux_session).kill();
|
|
942
|
+
const backend = (0, index_js_2.resolveBackend)(config, task.agent_backend);
|
|
943
|
+
const logDir = state.getLogDir(config.repo, task.issue);
|
|
944
|
+
const stateDir = state.getTaskStateDir(config.repo, task.issue);
|
|
945
|
+
const launchParams = {
|
|
946
|
+
worktree: task.worktree,
|
|
947
|
+
issue_url: `https://github.com/${task.repo}/issues/${task.issue}`,
|
|
948
|
+
issue_number: task.issue,
|
|
949
|
+
repo: task.repo,
|
|
950
|
+
title: task.title,
|
|
951
|
+
body: humanText,
|
|
952
|
+
labels: [],
|
|
953
|
+
log_dir: logDir,
|
|
954
|
+
state_dir: stateDir,
|
|
955
|
+
permission_mode: '--dangerously-skip-permissions',
|
|
956
|
+
};
|
|
957
|
+
const command = backend.resolveCommand(launchParams);
|
|
958
|
+
await worker.spawn({
|
|
959
|
+
id: task.tmux_session,
|
|
960
|
+
cwd: task.worktree,
|
|
961
|
+
command,
|
|
962
|
+
env: await this.launchEnv(this.briefFor(task), backend.resolveEnv(launchParams), {
|
|
963
|
+
FOUNDRY_RESUME_CONTEXT: buildResumeContext(task, humanText, kind),
|
|
964
|
+
}),
|
|
965
|
+
});
|
|
966
|
+
state.updateTaskStatus(config.repo, task.issue, 'agent-running', {
|
|
967
|
+
permission_mode: '--dangerously-skip-permissions',
|
|
968
|
+
});
|
|
969
|
+
log.success(`Relaunched agent for #${task.issue} in ${task.tmux_session} (${kind})`);
|
|
970
|
+
}
|
|
971
|
+
/** A review requested changes: relaunch with the review, within the input-round cap. */
|
|
972
|
+
async resumeAgentForPR(task, feedback) {
|
|
973
|
+
const config = this.config;
|
|
974
|
+
const inputRound = (task.input_request_count ?? 0) + 1;
|
|
975
|
+
if (inputRound > config.max_input_rounds) {
|
|
976
|
+
log.error(`#${task.issue} exceeded max input rounds (${config.max_input_rounds}) for PR feedback. Needs manual intervention.`);
|
|
977
|
+
state.updateTaskStatus(config.repo, task.issue, 'failed', { input_request_count: inputRound });
|
|
978
|
+
await claim.markFailed(config, task.issue);
|
|
979
|
+
const prGiveUpTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'failed' };
|
|
980
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
981
|
+
task: prGiveUpTask,
|
|
982
|
+
message: `Exceeded ${config.max_input_rounds} input rounds for PR feedback.\n\n${exports.FAILED_FOOTER}`,
|
|
983
|
+
historyEntry: `Gave up after ${config.max_input_rounds} PR feedback rounds`,
|
|
984
|
+
});
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
await this.relaunch(task, feedback, 'review');
|
|
988
|
+
state.updateTaskStatus(config.repo, task.issue, 'agent-running', {
|
|
989
|
+
input_request_count: inputRound,
|
|
990
|
+
permission_mode: '--dangerously-skip-permissions',
|
|
991
|
+
});
|
|
992
|
+
const prFeedbackTask = state.getAllTasks(config.repo).find(t => t.issue === task.issue) ?? { ...task, status: 'agent-running' };
|
|
993
|
+
await statusComment.updateStatusComment(config.repo, task.issue, {
|
|
994
|
+
task: prFeedbackTask,
|
|
995
|
+
message: `Agent is addressing PR review feedback (round ${inputRound}). \`@foundry stop\` to stop it.`,
|
|
996
|
+
historyEntry: `Resumed for PR feedback (round ${inputRound})`,
|
|
997
|
+
});
|
|
998
|
+
log.success(`Resumed agent for #${task.issue} to address PR feedback (round ${inputRound})`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
exports.EventHandler = EventHandler;
|
|
1002
|
+
//# sourceMappingURL=event-handler.js.map
|