@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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. 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 };