@workermill/agent 0.7.10 → 0.7.12

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.
@@ -39,7 +39,7 @@ export interface CriticResult {
39
39
  suggestedChanges?: string[];
40
40
  }>;
41
41
  }
42
- declare const AUTO_APPROVAL_THRESHOLD = 80;
42
+ declare const AUTO_APPROVAL_THRESHOLD = 85;
43
43
  /**
44
44
  * Parse execution plan JSON from raw Claude CLI output.
45
45
  * Mirrors server-side parseExecutionPlan() in planning-agent-local.ts.
@@ -61,6 +61,18 @@ export declare function applyStoryCap(plan: ExecutionPlan, maxStories: number):
61
61
  droppedCount: number;
62
62
  details: string[];
63
63
  };
64
+ /**
65
+ * Resolve file overlaps by assigning each shared file to exactly one story.
66
+ * When multiple stories list the same targetFile, the first story keeps it
67
+ * and it's removed from subsequent stories. This prevents parallel merge
68
+ * conflicts during consolidation — same auto-fix pattern as applyFileCap.
69
+ *
70
+ * Returns details about resolved overlaps for logging.
71
+ */
72
+ export declare function resolveFileOverlaps(plan: ExecutionPlan): {
73
+ resolvedCount: number;
74
+ details: string[];
75
+ };
64
76
  /**
65
77
  * Re-serialize plan as a JSON code block for posting to the API.
66
78
  * The server-side parseExecutionPlan() expects ```json ... ``` blocks.
@@ -16,7 +16,7 @@ import { generateText } from "./providers.js";
16
16
  // CONSTANTS
17
17
  // ============================================================================
18
18
  const MAX_TARGET_FILES = 5;
19
- const AUTO_APPROVAL_THRESHOLD = 80;
19
+ const AUTO_APPROVAL_THRESHOLD = 85;
20
20
  // ============================================================================
21
21
  // PLAN PARSING
22
22
  // ============================================================================
@@ -138,6 +138,45 @@ export function applyStoryCap(plan, maxStories) {
138
138
  return { droppedCount, details };
139
139
  }
140
140
  // ============================================================================
141
+ // FILE OVERLAP VALIDATION
142
+ // ============================================================================
143
+ /**
144
+ * Resolve file overlaps by assigning each shared file to exactly one story.
145
+ * When multiple stories list the same targetFile, the first story keeps it
146
+ * and it's removed from subsequent stories. This prevents parallel merge
147
+ * conflicts during consolidation — same auto-fix pattern as applyFileCap.
148
+ *
149
+ * Returns details about resolved overlaps for logging.
150
+ */
151
+ export function resolveFileOverlaps(plan) {
152
+ const fileOwner = new Map(); // file → first story that claims it
153
+ let resolvedCount = 0;
154
+ const details = [];
155
+ for (const story of plan.stories) {
156
+ if (!story.targetFiles || story.targetFiles.length === 0)
157
+ continue;
158
+ const kept = [];
159
+ const removed = [];
160
+ for (const file of story.targetFiles) {
161
+ const owner = fileOwner.get(file);
162
+ if (owner) {
163
+ // File already claimed by an earlier story — remove from this one
164
+ removed.push(file);
165
+ }
166
+ else {
167
+ fileOwner.set(file, story.id);
168
+ kept.push(file);
169
+ }
170
+ }
171
+ if (removed.length > 0) {
172
+ story.targetFiles = kept;
173
+ resolvedCount += removed.length;
174
+ details.push(`${story.id}: removed ${removed.join(", ")} (owned by ${removed.map((f) => fileOwner.get(f)).join(", ")})`);
175
+ }
176
+ }
177
+ return { resolvedCount, details };
178
+ }
179
+ // ============================================================================
141
180
  // PLAN SERIALIZATION
142
181
  // ============================================================================
143
182
  /**
package/dist/planner.js CHANGED
@@ -19,7 +19,7 @@ import ora from "ora";
19
19
  import { spawn, execSync } from "child_process";
20
20
  import { findClaudePath } from "./config.js";
21
21
  import { api } from "./api.js";
22
- import { parseExecutionPlan, applyFileCap, applyStoryCap, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
22
+ import { parseExecutionPlan, applyFileCap, applyStoryCap, resolveFileOverlaps, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
23
23
  import { generateTextWithTools } from "./ai-sdk-generate.js";
24
24
  /** Max Planner-Critic iterations before giving up */
25
25
  const MAX_ITERATIONS = 3;
@@ -881,6 +881,16 @@ export async function planTask(task, config, credentials) {
881
881
  console.log(`${ts()} ${taskLabel} ${chalk.dim(detail)}`);
882
882
  }
883
883
  }
884
+ // 2c3. Resolve file overlaps (assign each shared file to first story only)
885
+ const { resolvedCount: overlapCount, details: overlapDetails } = resolveFileOverlaps(plan);
886
+ if (overlapCount > 0) {
887
+ const msg = `${PREFIX} File overlap resolved: ${overlapCount} shared file(s) de-duped across stories`;
888
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
889
+ await postLog(task.id, msg);
890
+ for (const detail of overlapDetails) {
891
+ console.log(`${ts()} ${taskLabel} ${chalk.dim(detail)}`);
892
+ }
893
+ }
884
894
  console.log(`${ts()} ${taskLabel} Plan: ${chalk.bold(plan.stories.length)} stories (max ${maxStories})`);
885
895
  await postLog(task.id, `${PREFIX} Plan generated: ${plan.stories.length} stories (${formatElapsed(elapsed)}). Running critic validation...`);
886
896
  // 2d. Run critic validation
package/dist/spawner.js CHANGED
@@ -280,6 +280,7 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
280
280
  process: proc,
281
281
  startedAt: new Date(),
282
282
  status: "running",
283
+ resultEmitted: false,
283
284
  };
284
285
  activeContainers.set(task.id, container);
285
286
  // Stream stdout/stderr to console (logs go to cloud via container's own HTTP calls)
@@ -287,6 +288,10 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
287
288
  const lines = data.toString().split("\n").filter((l) => l.trim());
288
289
  for (const line of lines) {
289
290
  console.log(`${ts()} ${taskLabel} ${chalk.dim(line)}`);
291
+ // Track if worker emitted ::result:: marker (means it called worker-complete itself)
292
+ if (line.includes("::result::")) {
293
+ container.resultEmitted = true;
294
+ }
290
295
  }
291
296
  });
292
297
  proc.stderr?.on("data", (data) => {
@@ -302,8 +307,41 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
302
307
  const icon = code === 0 ? chalk.green("✓") : chalk.red("✗");
303
308
  const status = code === 0 ? chalk.green("completed") : chalk.red(`failed (exit ${code})`);
304
309
  console.log(`${ts()} ${taskLabel} ${icon} Container ${status} ${chalk.dim(`(${duration}s)`)}`);
305
- // Clean up after delay
306
- setTimeout(() => activeContainers.delete(task.id), 60_000);
310
+ // Safety net: if worker didn't emit ::result::, it may have died without calling worker-complete.
311
+ // Wait briefly for any in-flight API calls, then POST a fallback completion.
312
+ if (!container.resultEmitted) {
313
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No ::result:: marker seen — posting fallback completion in 15s`);
314
+ setTimeout(async () => {
315
+ try {
316
+ const fallbackResult = code === 0 ? "completed" : "failed";
317
+ const errorMsg = code !== 0 ? `Worker container exited with code ${code} without reporting completion` : undefined;
318
+ const resp = await fetch(`${config.apiUrl}/api/tasks/${task.id}/worker-complete`, {
319
+ method: "POST",
320
+ headers: {
321
+ "Content-Type": "application/json",
322
+ "x-api-key": config.apiKey,
323
+ },
324
+ body: JSON.stringify({
325
+ exitCode: code ?? 1,
326
+ result: fallbackResult,
327
+ errorMessage: errorMsg,
328
+ }),
329
+ });
330
+ const data = await resp.json();
331
+ if (data.status === "ignored") {
332
+ console.log(`${ts()} ${taskLabel} ${chalk.dim("Fallback completion ignored (task already transitioned)")}`);
333
+ }
334
+ else {
335
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} Fallback completion applied: ${fallbackResult}`);
336
+ }
337
+ }
338
+ catch (err) {
339
+ console.error(`${ts()} ${taskLabel} ${chalk.red("✗")} Fallback completion failed:`, err instanceof Error ? err.message : err);
340
+ }
341
+ }, 15_000);
342
+ }
343
+ // Clean up after delay (extended to allow fallback completion to finish)
344
+ setTimeout(() => activeContainers.delete(task.id), 90_000);
307
345
  });
308
346
  proc.on("error", (err) => {
309
347
  container.status = "failed";
@@ -444,6 +482,7 @@ export async function spawnManagerWorker(task, config, credentials) {
444
482
  process: proc,
445
483
  startedAt: new Date(),
446
484
  status: "running",
485
+ resultEmitted: false,
447
486
  };
448
487
  activeContainers.set(managerKey, container);
449
488
  proc.stdout?.on("data", (data) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",