@virtengine/openfleet 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-assessment.mjs — Codex/Copilot SDK-powered task lifecycle assessment.
|
|
3
|
+
*
|
|
4
|
+
* Provides intelligent decision-making for task lifecycle events:
|
|
5
|
+
* 1. Should we merge this PR?
|
|
6
|
+
* 2. Should we reprompt the same agent session?
|
|
7
|
+
* 3. Should we start a new session (same agent)?
|
|
8
|
+
* 4. Should we start a completely new attempt (different agent)?
|
|
9
|
+
* 5. What EXACTLY should the prompt say?
|
|
10
|
+
*
|
|
11
|
+
* Unlike merge-strategy.mjs (which only runs post-completion), this module
|
|
12
|
+
* provides continuous assessment throughout the task lifecycle — including
|
|
13
|
+
* during rebase failures, idle detection, and post-merge downstream effects.
|
|
14
|
+
*
|
|
15
|
+
* Decisions are structured JSON with dynamic prompt generation.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { execSync } from "node:child_process";
|
|
21
|
+
|
|
22
|
+
// ── Valid lifecycle actions ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const VALID_ACTIONS = new Set([
|
|
25
|
+
"merge", // PR is ready — merge when CI passes
|
|
26
|
+
"reprompt_same", // Send follow-up to the SAME agent session
|
|
27
|
+
"reprompt_new_session", // Kill current session, start fresh session (same attempt)
|
|
28
|
+
"new_attempt", // Abandon attempt entirely, start fresh attempt with new agent
|
|
29
|
+
"wait", // Wait N seconds then re-assess
|
|
30
|
+
"manual_review", // Escalate to human
|
|
31
|
+
"close_and_replan", // Close PR, move task back to todo for replanning
|
|
32
|
+
"noop", // No action needed
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// ── Dedup / rate limiting ───────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** @type {Map<string, number>} taskId → last assessment timestamp */
|
|
38
|
+
const assessmentDedup = new Map();
|
|
39
|
+
const ASSESSMENT_COOLDOWN_MS = 5 * 60 * 1000; // 5 min per task
|
|
40
|
+
|
|
41
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} TaskAssessmentContext
|
|
45
|
+
* @property {string} taskId - Task UUID
|
|
46
|
+
* @property {string} taskTitle - Task title
|
|
47
|
+
* @property {string} [taskDescription] - Task description
|
|
48
|
+
* @property {string} attemptId - Attempt UUID
|
|
49
|
+
* @property {string} shortId - Short ID for logging
|
|
50
|
+
* @property {string} trigger - What triggered the assessment
|
|
51
|
+
* ("rebase_failed", "idle_detected", "pr_merged_downstream", "agent_completed",
|
|
52
|
+
* "agent_failed", "ci_failed", "conflict_detected", "manual_request")
|
|
53
|
+
* @property {string} [branch] - Branch name
|
|
54
|
+
* @property {string} [upstreamBranch] - Target/base branch
|
|
55
|
+
* @property {string} [agentLastMessage] - Last message from agent
|
|
56
|
+
* @property {string} [agentType] - "codex" or "copilot"
|
|
57
|
+
* @property {number} [attemptCount] - Number of attempts so far
|
|
58
|
+
* @property {number} [sessionRetries] - Number of session retries
|
|
59
|
+
* @property {number} [prNumber] - PR number if exists
|
|
60
|
+
* @property {string} [prState] - PR state
|
|
61
|
+
* @property {string} [ciStatus] - CI status
|
|
62
|
+
* @property {string} [rebaseError] - Error message from failed rebase
|
|
63
|
+
* @property {string[]} [conflictFiles] - List of conflicted files
|
|
64
|
+
* @property {string} [diffStat] - Git diff stats
|
|
65
|
+
* @property {number} [commitsAhead] - Commits ahead of upstream
|
|
66
|
+
* @property {number} [commitsBehind] - Commits behind upstream
|
|
67
|
+
* @property {number} [taskAgeHours] - How old the task is in hours
|
|
68
|
+
* @property {object} [previousDecisions] - History of past decisions for this task
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {object} TaskAssessmentDecision
|
|
73
|
+
* @property {boolean} success - Whether assessment completed
|
|
74
|
+
* @property {string} action - One of VALID_ACTIONS
|
|
75
|
+
* @property {string} [prompt] - Dynamic prompt to send (for reprompt_same/reprompt_new_session)
|
|
76
|
+
* @property {string} [reason] - Explanation for the decision
|
|
77
|
+
* @property {number} [waitSeconds] - For "wait" action
|
|
78
|
+
* @property {string} [agentType] - Preferred agent for new_attempt ("codex" | "copilot")
|
|
79
|
+
* @property {string} rawOutput - Raw SDK output for audit
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
// ── Prompt builder ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the assessment prompt based on the trigger and context.
|
|
86
|
+
*/
|
|
87
|
+
function buildAssessmentPrompt(ctx) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
|
|
90
|
+
parts.push(`# Task Lifecycle Assessment
|
|
91
|
+
|
|
92
|
+
You are an expert autonomous engineering orchestrator. You must decide the BEST next action
|
|
93
|
+
for a task based on the context below. Your goal is to maximize task completion rate while
|
|
94
|
+
minimizing wasted compute.
|
|
95
|
+
|
|
96
|
+
## Trigger
|
|
97
|
+
**Event:** ${ctx.trigger}
|
|
98
|
+
**Timestamp:** ${new Date().toISOString()}
|
|
99
|
+
|
|
100
|
+
## Task Context`);
|
|
101
|
+
|
|
102
|
+
if (ctx.taskTitle) parts.push(`**Task:** ${ctx.taskTitle}`);
|
|
103
|
+
if (ctx.taskDescription) {
|
|
104
|
+
parts.push(`**Description:** ${ctx.taskDescription.slice(0, 3000)}`);
|
|
105
|
+
}
|
|
106
|
+
if (ctx.branch) parts.push(`**Branch:** ${ctx.branch}`);
|
|
107
|
+
if (ctx.upstreamBranch)
|
|
108
|
+
parts.push(`**Upstream/Base:** ${ctx.upstreamBranch}`);
|
|
109
|
+
if (ctx.agentType) parts.push(`**Agent:** ${ctx.agentType}`);
|
|
110
|
+
if (ctx.attemptCount != null)
|
|
111
|
+
parts.push(`**Attempt #:** ${ctx.attemptCount}`);
|
|
112
|
+
if (ctx.sessionRetries != null)
|
|
113
|
+
parts.push(`**Session Retries:** ${ctx.sessionRetries}`);
|
|
114
|
+
if (ctx.taskAgeHours != null)
|
|
115
|
+
parts.push(`**Task Age:** ${ctx.taskAgeHours.toFixed(1)}h`);
|
|
116
|
+
|
|
117
|
+
// Trigger-specific context
|
|
118
|
+
if (ctx.trigger === "rebase_failed" && ctx.rebaseError) {
|
|
119
|
+
parts.push(`
|
|
120
|
+
## Rebase Failure Details
|
|
121
|
+
\`\`\`
|
|
122
|
+
${ctx.rebaseError.slice(0, 4000)}
|
|
123
|
+
\`\`\``);
|
|
124
|
+
if (ctx.conflictFiles?.length) {
|
|
125
|
+
parts.push(`
|
|
126
|
+
### Conflicted Files
|
|
127
|
+
${ctx.conflictFiles.map((f) => `- ${f}`).join("\n")}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ctx.trigger === "pr_merged_downstream") {
|
|
132
|
+
parts.push(`
|
|
133
|
+
## Downstream Impact
|
|
134
|
+
A PR was just merged into the upstream branch (${ctx.upstreamBranch}).
|
|
135
|
+
This task's branch needs to be rebased to incorporate the changes.
|
|
136
|
+
The rebase ${ctx.rebaseError ? "FAILED" : "has not been attempted yet"}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Agent's last message
|
|
140
|
+
if (ctx.agentLastMessage) {
|
|
141
|
+
parts.push(`
|
|
142
|
+
## Agent's Last Message
|
|
143
|
+
\`\`\`
|
|
144
|
+
${ctx.agentLastMessage.slice(0, 6000)}
|
|
145
|
+
\`\`\``);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// PR details
|
|
149
|
+
if (ctx.prNumber) {
|
|
150
|
+
parts.push(`
|
|
151
|
+
## Pull Request
|
|
152
|
+
- PR #${ctx.prNumber}
|
|
153
|
+
- State: ${ctx.prState || "unknown"}
|
|
154
|
+
- CI: ${ctx.ciStatus || "unknown"}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Diff context
|
|
158
|
+
if (ctx.commitsAhead != null || ctx.commitsBehind != null) {
|
|
159
|
+
parts.push(`
|
|
160
|
+
## Branch Status
|
|
161
|
+
- Commits ahead: ${ctx.commitsAhead ?? "unknown"}
|
|
162
|
+
- Commits behind: ${ctx.commitsBehind ?? "unknown"}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ctx.diffStat) {
|
|
166
|
+
parts.push(`
|
|
167
|
+
### Diff Stats
|
|
168
|
+
\`\`\`
|
|
169
|
+
${ctx.diffStat.slice(0, 2000)}
|
|
170
|
+
\`\`\``);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Decision history
|
|
174
|
+
if (ctx.previousDecisions) {
|
|
175
|
+
const history = JSON.stringify(ctx.previousDecisions, null, 2).slice(
|
|
176
|
+
0,
|
|
177
|
+
1500,
|
|
178
|
+
);
|
|
179
|
+
parts.push(`
|
|
180
|
+
## Previous Decisions
|
|
181
|
+
\`\`\`json
|
|
182
|
+
${history}
|
|
183
|
+
\`\`\``);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Decision framework — adapted per trigger
|
|
187
|
+
parts.push(`
|
|
188
|
+
## Decision Rules
|
|
189
|
+
|
|
190
|
+
Choose ONE action based on the trigger "${ctx.trigger}":
|
|
191
|
+
|
|
192
|
+
### Actions Available
|
|
193
|
+
|
|
194
|
+
1. **merge** — The PR is ready to merge. Agent completed the work, CI passing or expected to pass.
|
|
195
|
+
Generate: \`{ "action": "merge", "reason": "..." }\`
|
|
196
|
+
|
|
197
|
+
2. **reprompt_same** — Send a SPECIFIC follow-up message to the same agent session.
|
|
198
|
+
The agent is still running and can receive messages. Use when:
|
|
199
|
+
- Small fix needed (lint error, missing test, typo)
|
|
200
|
+
- Rebase conflict on files the agent can resolve
|
|
201
|
+
- Agent needs to push their changes
|
|
202
|
+
Generate: \`{ "action": "reprompt_same", "prompt": "SPECIFIC instructions for the agent...", "reason": "..." }\`
|
|
203
|
+
|
|
204
|
+
3. **reprompt_new_session** — Kill current session, start fresh with the same task.
|
|
205
|
+
Use when:
|
|
206
|
+
- Agent's context window is exhausted
|
|
207
|
+
- Agent is stuck in a loop
|
|
208
|
+
- Session has accumulated too many errors
|
|
209
|
+
- Rebase failed and agent needs a clean start to resolve
|
|
210
|
+
Generate: \`{ "action": "reprompt_new_session", "prompt": "SPECIFIC task instructions for fresh session...", "reason": "..." }\`
|
|
211
|
+
|
|
212
|
+
4. **new_attempt** — Completely fresh attempt, potentially different agent type.
|
|
213
|
+
Use when:
|
|
214
|
+
- Multiple session retries have failed (>2)
|
|
215
|
+
- Agent consistently misunderstands the task
|
|
216
|
+
- Need to switch between Codex and Copilot
|
|
217
|
+
Generate: \`{ "action": "new_attempt", "reason": "...", "agentType": "codex"|"copilot" }\`
|
|
218
|
+
|
|
219
|
+
5. **wait** — Re-assess after N seconds.
|
|
220
|
+
Use when: CI running, rebase in progress, agent actively working.
|
|
221
|
+
Generate: \`{ "action": "wait", "waitSeconds": 300, "reason": "..." }\`
|
|
222
|
+
|
|
223
|
+
6. **manual_review** — Escalate to human.
|
|
224
|
+
Use when: Security-sensitive changes, complex conflicts, repeated failures.
|
|
225
|
+
Generate: \`{ "action": "manual_review", "reason": "..." }\`
|
|
226
|
+
|
|
227
|
+
7. **close_and_replan** — Close PR, move task back to backlog for replanning.
|
|
228
|
+
Use when: Approach is fundamentally wrong, task needs rethinking.
|
|
229
|
+
Generate: \`{ "action": "close_and_replan", "reason": "..." }\`
|
|
230
|
+
|
|
231
|
+
8. **noop** — No action needed.
|
|
232
|
+
Generate: \`{ "action": "noop", "reason": "..." }\`
|
|
233
|
+
|
|
234
|
+
### CRITICAL Rules for Prompt Generation
|
|
235
|
+
|
|
236
|
+
When generating prompts (for reprompt_same or reprompt_new_session), the prompt MUST:
|
|
237
|
+
- Be SPECIFIC — include file names, error messages, exact instructions
|
|
238
|
+
- Include the task context — the agent may have lost context
|
|
239
|
+
- For rebase failures: instruct the agent to resolve specific conflicts
|
|
240
|
+
- For CI failures: paste the error output and tell the agent what to fix
|
|
241
|
+
- NEVER be generic like "please fix the issues" — that wastes compute time
|
|
242
|
+
|
|
243
|
+
## Response Format
|
|
244
|
+
|
|
245
|
+
Respond with ONLY a JSON object:
|
|
246
|
+
|
|
247
|
+
\`\`\`json
|
|
248
|
+
{
|
|
249
|
+
"action": "reprompt_same",
|
|
250
|
+
"prompt": "The rebase onto origin/staging failed with conflicts in go.sum and pnpm-lock.yaml. Run 'git checkout --theirs go.sum pnpm-lock.yaml && git add go.sum pnpm-lock.yaml && git rebase --continue' to resolve. Then run tests and push.",
|
|
251
|
+
"reason": "Rebase conflict on auto-resolvable lock files. Agent can fix in current session."
|
|
252
|
+
}
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
RESPOND WITH ONLY THE JSON OBJECT.`);
|
|
256
|
+
|
|
257
|
+
return parts.join("\n");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── JSON extraction (shared pattern with merge-strategy.mjs) ────────────────
|
|
261
|
+
|
|
262
|
+
function extractDecisionJson(raw) {
|
|
263
|
+
if (!raw || typeof raw !== "string") return null;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const parsed = JSON.parse(raw.trim());
|
|
267
|
+
if (parsed && typeof parsed.action === "string") return parsed;
|
|
268
|
+
} catch {
|
|
269
|
+
/* not pure JSON */
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
273
|
+
if (fenceMatch) {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(fenceMatch[1].trim());
|
|
276
|
+
if (parsed && typeof parsed.action === "string") return parsed;
|
|
277
|
+
} catch {
|
|
278
|
+
/* bad JSON in fence */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const braceMatch = raw.match(/\{[\s\S]*?"action"\s*:\s*"[^"]+?"[\s\S]*?\}/);
|
|
283
|
+
if (braceMatch) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(braceMatch[0]);
|
|
286
|
+
if (parsed && typeof parsed.action === "string") return parsed;
|
|
287
|
+
} catch {
|
|
288
|
+
/* partial match */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Main assessment function ────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Assess a task and return a structured lifecycle decision.
|
|
299
|
+
*
|
|
300
|
+
* @param {TaskAssessmentContext} ctx
|
|
301
|
+
* @param {object} opts
|
|
302
|
+
* @param {function} opts.execCodex - Primary agent prompt executor
|
|
303
|
+
* @param {number} [opts.timeoutMs] - Timeout for SDK call
|
|
304
|
+
* @param {string} [opts.logDir] - Directory for audit logs
|
|
305
|
+
* @param {function} [opts.onTelegram] - Telegram notification callback
|
|
306
|
+
* @returns {Promise<TaskAssessmentDecision>}
|
|
307
|
+
*/
|
|
308
|
+
export async function assessTask(ctx, opts) {
|
|
309
|
+
const tag = `assessment(${ctx.shortId})`;
|
|
310
|
+
|
|
311
|
+
// ── Dedup check ─────────────────────────────────────────────
|
|
312
|
+
const lastRun = assessmentDedup.get(ctx.taskId);
|
|
313
|
+
if (lastRun && Date.now() - lastRun < ASSESSMENT_COOLDOWN_MS) {
|
|
314
|
+
console.log(
|
|
315
|
+
`[${tag}] skipping — assessed ${Math.round((Date.now() - lastRun) / 1000)}s ago`,
|
|
316
|
+
);
|
|
317
|
+
return { success: false, action: "noop", reason: "dedup", rawOutput: "" };
|
|
318
|
+
}
|
|
319
|
+
assessmentDedup.set(ctx.taskId, Date.now());
|
|
320
|
+
|
|
321
|
+
const timeoutMs = opts.timeoutMs || 5 * 60 * 1000;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// ── Build prompt ──────────────────────────────────────────
|
|
325
|
+
const prompt = buildAssessmentPrompt(ctx);
|
|
326
|
+
|
|
327
|
+
// ── Execute via primary agent SDK ─────────────────────────
|
|
328
|
+
console.log(`[${tag}] running assessment (trigger: ${ctx.trigger})`);
|
|
329
|
+
const result = await opts.execCodex(prompt, { timeoutMs });
|
|
330
|
+
|
|
331
|
+
const rawOutput = result?.finalResponse || result || "";
|
|
332
|
+
const rawStr =
|
|
333
|
+
typeof rawOutput === "string" ? rawOutput : JSON.stringify(rawOutput);
|
|
334
|
+
|
|
335
|
+
// ── Parse decision ────────────────────────────────────────
|
|
336
|
+
const decision = extractDecisionJson(rawStr);
|
|
337
|
+
|
|
338
|
+
if (!decision || !VALID_ACTIONS.has(decision.action)) {
|
|
339
|
+
console.warn(
|
|
340
|
+
`[${tag}] invalid/missing action in response — defaulting to manual_review`,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Write audit log
|
|
344
|
+
await writeAuditLog(opts.logDir, ctx, rawStr, {
|
|
345
|
+
action: "manual_review",
|
|
346
|
+
reason: "parse_failure",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
action: "manual_review",
|
|
352
|
+
reason: `Could not parse assessment response: ${rawStr.slice(0, 200)}`,
|
|
353
|
+
rawOutput: rawStr,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const result_ = {
|
|
358
|
+
success: true,
|
|
359
|
+
action: decision.action,
|
|
360
|
+
prompt: decision.prompt || decision.message || undefined,
|
|
361
|
+
reason: decision.reason || undefined,
|
|
362
|
+
waitSeconds: decision.waitSeconds || decision.seconds || undefined,
|
|
363
|
+
agentType: decision.agentType || undefined,
|
|
364
|
+
rawOutput: rawStr,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// ── Audit log ─────────────────────────────────────────────
|
|
368
|
+
await writeAuditLog(opts.logDir, ctx, rawStr, result_);
|
|
369
|
+
|
|
370
|
+
// ── Telegram notification ─────────────────────────────────
|
|
371
|
+
if (opts.onTelegram) {
|
|
372
|
+
const emoji =
|
|
373
|
+
{
|
|
374
|
+
merge: "✅",
|
|
375
|
+
reprompt_same: "💬",
|
|
376
|
+
reprompt_new_session: "🔄",
|
|
377
|
+
new_attempt: "🆕",
|
|
378
|
+
wait: "⏳",
|
|
379
|
+
manual_review: "👀",
|
|
380
|
+
close_and_replan: "🚫",
|
|
381
|
+
noop: "⚪",
|
|
382
|
+
}[decision.action] || "❓";
|
|
383
|
+
opts.onTelegram(
|
|
384
|
+
`${emoji} Assessment [${ctx.shortId}] ${ctx.trigger}: **${decision.action}**\n${decision.reason || ""}`.slice(
|
|
385
|
+
0,
|
|
386
|
+
500,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log(
|
|
392
|
+
`[${tag}] decision: ${decision.action} — ${(decision.reason || "").slice(0, 100)}`,
|
|
393
|
+
);
|
|
394
|
+
return result_;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.warn(`[${tag}] assessment error: ${err.message || err}`);
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
action: "noop",
|
|
400
|
+
reason: `Assessment error: ${err.message || err}`,
|
|
401
|
+
rawOutput: "",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Quick assessment (no SDK call) ──────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Fast heuristic-based assessment for common scenarios that don't need SDK.
|
|
410
|
+
* Returns a decision if the scenario is clear-cut, or null if SDK is needed.
|
|
411
|
+
*
|
|
412
|
+
* @param {TaskAssessmentContext} ctx
|
|
413
|
+
* @returns {TaskAssessmentDecision | null}
|
|
414
|
+
*/
|
|
415
|
+
export function quickAssess(ctx) {
|
|
416
|
+
// ── Rebase failed on only auto-resolvable files ──────────
|
|
417
|
+
if (ctx.trigger === "rebase_failed" && ctx.conflictFiles?.length) {
|
|
418
|
+
const lockPatterns = [
|
|
419
|
+
"pnpm-lock.yaml",
|
|
420
|
+
"package-lock.json",
|
|
421
|
+
"yarn.lock",
|
|
422
|
+
"go.sum",
|
|
423
|
+
"CHANGELOG.md",
|
|
424
|
+
"coverage.txt",
|
|
425
|
+
"results.txt",
|
|
426
|
+
];
|
|
427
|
+
const lockExts = [".lock"];
|
|
428
|
+
const allAutoResolvable = ctx.conflictFiles.every((f) => {
|
|
429
|
+
const name = f.split("/").pop();
|
|
430
|
+
return (
|
|
431
|
+
lockPatterns.includes(name) ||
|
|
432
|
+
lockExts.some((ext) => name.endsWith(ext))
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (allAutoResolvable) {
|
|
437
|
+
const theirsFiles = ctx.conflictFiles.filter((f) => {
|
|
438
|
+
const name = f.split("/").pop();
|
|
439
|
+
return !["CHANGELOG.md", "coverage.txt", "results.txt"].includes(name);
|
|
440
|
+
});
|
|
441
|
+
const oursFiles = ctx.conflictFiles.filter((f) => {
|
|
442
|
+
const name = f.split("/").pop();
|
|
443
|
+
return ["CHANGELOG.md", "coverage.txt", "results.txt"].includes(name);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const instructions = [];
|
|
447
|
+
if (theirsFiles.length) {
|
|
448
|
+
instructions.push(
|
|
449
|
+
`git checkout --theirs ${theirsFiles.join(" ")} && git add ${theirsFiles.join(" ")}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (oursFiles.length) {
|
|
453
|
+
instructions.push(
|
|
454
|
+
`git checkout --ours ${oursFiles.join(" ")} && git add ${oursFiles.join(" ")}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
success: true,
|
|
460
|
+
action: "reprompt_same",
|
|
461
|
+
prompt: `Rebase onto ${ctx.upstreamBranch || "upstream"} failed with conflicts in auto-resolvable files. Run:\n${instructions.join("\n")}\nThen run: git rebase --continue\nAfter that, run tests and push.`,
|
|
462
|
+
reason: `All ${ctx.conflictFiles.length} conflicted files are auto-resolvable (lock files/generated)`,
|
|
463
|
+
rawOutput: "quick_assess:auto_resolvable_conflicts",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Too many attempts — escalate ─────────────────────────
|
|
469
|
+
if (ctx.attemptCount != null && ctx.attemptCount >= 4) {
|
|
470
|
+
return {
|
|
471
|
+
success: true,
|
|
472
|
+
action: "manual_review",
|
|
473
|
+
reason: `Task has had ${ctx.attemptCount} attempts — escalating to human review`,
|
|
474
|
+
rawOutput: "quick_assess:max_attempts",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── Too many session retries — try new attempt ───────────
|
|
479
|
+
if (ctx.sessionRetries != null && ctx.sessionRetries >= 3) {
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
action: "new_attempt",
|
|
483
|
+
reason: `${ctx.sessionRetries} session retries exhausted — starting fresh attempt with alternate agent`,
|
|
484
|
+
agentType: ctx.agentType === "codex" ? "copilot" : "codex",
|
|
485
|
+
rawOutput: "quick_assess:session_retries_exhausted",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── PR merged downstream — always rebase first ───────────
|
|
490
|
+
if (ctx.trigger === "pr_merged_downstream" && !ctx.rebaseError) {
|
|
491
|
+
return {
|
|
492
|
+
success: true,
|
|
493
|
+
action: "reprompt_same",
|
|
494
|
+
prompt: `A PR was just merged into your upstream branch (${ctx.upstreamBranch}). Please rebase your branch onto ${ctx.upstreamBranch} to incorporate the latest changes: git fetch origin && git rebase ${ctx.upstreamBranch}. Resolve any conflicts, then push.`,
|
|
495
|
+
reason: "Upstream branch updated — agent should rebase",
|
|
496
|
+
rawOutput: "quick_assess:downstream_rebase",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// No quick assessment possible — caller should use full SDK assessment
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Audit logging ───────────────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
async function writeAuditLog(logDir, ctx, rawOutput, decision) {
|
|
507
|
+
if (!logDir) return;
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await mkdir(logDir, { recursive: true });
|
|
511
|
+
|
|
512
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
513
|
+
const shortId = ctx.shortId || ctx.taskId?.substring(0, 8) || "unknown";
|
|
514
|
+
const filename = `assessment-${shortId}-${ctx.trigger}-${timestamp}.log`;
|
|
515
|
+
|
|
516
|
+
const content = [
|
|
517
|
+
`Task Assessment Audit Log`,
|
|
518
|
+
`========================`,
|
|
519
|
+
`Timestamp: ${new Date().toISOString()}`,
|
|
520
|
+
`Task: ${ctx.taskTitle || "unknown"} (${ctx.taskId || "unknown"})`,
|
|
521
|
+
`Attempt: ${ctx.attemptId || "unknown"} (${ctx.shortId || "unknown"})`,
|
|
522
|
+
`Trigger: ${ctx.trigger}`,
|
|
523
|
+
`Branch: ${ctx.branch || "unknown"}`,
|
|
524
|
+
`Upstream: ${ctx.upstreamBranch || "unknown"}`,
|
|
525
|
+
`Agent: ${ctx.agentType || "unknown"}`,
|
|
526
|
+
``,
|
|
527
|
+
`Decision:`,
|
|
528
|
+
` Action: ${decision.action}`,
|
|
529
|
+
` Reason: ${decision.reason || "none"}`,
|
|
530
|
+
decision.prompt ? ` Prompt: ${decision.prompt.slice(0, 500)}` : "",
|
|
531
|
+
``,
|
|
532
|
+
`Raw Output:`,
|
|
533
|
+
`${rawOutput}`,
|
|
534
|
+
]
|
|
535
|
+
.filter(Boolean)
|
|
536
|
+
.join("\n");
|
|
537
|
+
|
|
538
|
+
await writeFile(resolve(logDir, filename), content, "utf8");
|
|
539
|
+
} catch {
|
|
540
|
+
/* best-effort audit logging */
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
export function resetAssessmentDedup() {
|
|
547
|
+
assessmentDedup.clear();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export { VALID_ACTIONS, buildAssessmentPrompt, extractDecisionJson };
|