@femtomc/mu-orchestrator 26.2.19 → 26.2.21

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAEN,KAAK,QAAQ,EAKb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,MAAM,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,YAAY,GAAG,oBAAoB,GAAG,qBAAqB,GAAG,OAAO,CAAC;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,yBAAyB,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AA2BF,qBAAa,SAAS;;gBAWpB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAO;IAwKrF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC;CAiMjG"}
1
+ {"version":3,"file":"dag_runner.d.ts","sourceRoot":"","sources":["../src/dag_runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAEN,KAAK,QAAQ,EAKb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,uBAAuB,CAAC;AAGjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,MAAM,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,YAAY,GAAG,oBAAoB,GAAG,qBAAqB,GAAG,OAAO,CAAC;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,yBAAyB,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAkBF,qBAAa,SAAS;;gBAYpB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAO;IAiLrF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC;CAiNjG"}
@@ -2,20 +2,12 @@ import { mkdir } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
3
  import { currentRunId, fsEventLogFromRepoRoot, getStorePaths, newRunId, runContext, } from "@femtomc/mu-core/node";
4
4
  import { resolveModelConfig } from "./model_resolution.js";
5
- import { parseMuRole, systemPromptForRole } from "./mu_roles.js";
5
+ import { roleFromTags, systemPromptForRole } from "./mu_roles.js";
6
6
  import { PiSdkBackend } from "./pi_sdk_backend.js";
7
7
  function roundTo(n, digits) {
8
8
  const f = 10 ** digits;
9
9
  return Math.round(n * f) / f;
10
10
  }
11
- function specRoleFromExecutionSpec(execution_spec) {
12
- const role = execution_spec?.role;
13
- if (typeof role !== "string") {
14
- return null;
15
- }
16
- const trimmed = role.trim();
17
- return trimmed.length > 0 ? trimmed : null;
18
- }
19
11
  function relPath(repoRoot, path) {
20
12
  try {
21
13
  const rel = relative(repoRoot, path);
@@ -33,6 +25,7 @@ export class DagRunner {
33
25
  #backend;
34
26
  #modelOverrides;
35
27
  #reorchestrateOutcomes = new Set(["failure", "needs_work"]);
28
+ #attempts = new Map();
36
29
  constructor(store, forum, repoRoot, opts = {}) {
37
30
  this.#store = store;
38
31
  this.#forum = forum;
@@ -41,11 +34,10 @@ export class DagRunner {
41
34
  this.#backend = opts.backend ?? new PiSdkBackend();
42
35
  this.#modelOverrides = opts.modelOverrides ?? {};
43
36
  }
44
- async #resolveConfig(issue) {
45
- void issue;
37
+ async #resolveConfig() {
46
38
  return resolveModelConfig(this.#modelOverrides);
47
39
  }
48
- async #renderUserPrompt(issue, rootId, step) {
40
+ async #renderUserPrompt(issue, rootId, step, attempt = 1) {
49
41
  let rendered = issue.title ?? "";
50
42
  if (issue.body) {
51
43
  rendered += `\n\n${issue.body}`;
@@ -55,12 +47,15 @@ export class DagRunner {
55
47
  if (runId) {
56
48
  rendered += `Run: ${runId}\n`;
57
49
  }
50
+ if (attempt > 1) {
51
+ rendered += `\nAttempt: ${attempt} (previous attempt failed — check \`mu forum read issue:${issue.id}\` for context)\n`;
52
+ }
58
53
  return rendered;
59
54
  }
60
55
  async #executeBackend(issue, cfg, rootId, step, opts = {}) {
61
- const role = parseMuRole(specRoleFromExecutionSpec(issue.execution_spec));
56
+ const role = roleFromTags(issue.tags);
62
57
  const logSuffix = opts.logSuffix ?? "";
63
- const rendered = await this.#renderUserPrompt(issue, rootId, step);
58
+ const rendered = await this.#renderUserPrompt(issue, rootId, step, opts.attempt ?? 1);
64
59
  const systemPrompt = systemPromptForRole(role);
65
60
  const { logsDir } = getStorePaths(this.#repoRoot);
66
61
  await mkdir(logsDir, { recursive: true });
@@ -116,7 +111,7 @@ export class DagRunner {
116
111
  const reopened = await this.#store.update(issueId, {
117
112
  status: "open",
118
113
  outcome: null,
119
- execution_spec: { role: "orchestrator" },
114
+ tags: [...before.tags.filter((t) => !t.startsWith("role:")), "role:orchestrator"],
120
115
  });
121
116
  await this.#events.emit("dag.unstick.reopen", {
122
117
  source: "dag_runner",
@@ -152,6 +147,9 @@ export class DagRunner {
152
147
  continue;
153
148
  if (row.status !== "closed")
154
149
  continue;
150
+ // Circuit breaker: skip issues that have exhausted their attempts.
151
+ if ((this.#attempts.get(row.id) ?? 0) >= 3)
152
+ continue;
155
153
  const outcome = row.outcome;
156
154
  if (outcome && this.#reorchestrateOutcomes.has(outcome)) {
157
155
  if (hasOpenChildren(row.id))
@@ -215,9 +213,8 @@ export class DagRunner {
215
213
  ...rootIssue,
216
214
  title: `Repair stuck DAG: ${rootIssue.title}`,
217
215
  body: `${(rootIssue.body || "").trim()}\n\n## Runner Diagnostics\n\n${diag}`.trim(),
218
- execution_spec: null,
219
216
  };
220
- const cfg = await this.#resolveConfig(repairIssue);
217
+ const cfg = await this.#resolveConfig();
221
218
  const logSuffix = "unstick";
222
219
  const onBackendLine = hooks?.onBackendLine;
223
220
  const { exitCode, elapsedS } = await this.#executeBackend(repairIssue, cfg, rootId, step, {
@@ -243,8 +240,7 @@ export class DagRunner {
243
240
  }
244
241
  const issue = candidates[0];
245
242
  const issueId = issue.id;
246
- // Validate role early so we don't claim/work an unsupported leaf.
247
- const role = parseMuRole(specRoleFromExecutionSpec(issue.execution_spec));
243
+ const role = roleFromTags(issue.tags);
248
244
  await this.#events.emit("dag.step.start", {
249
245
  source: "dag_runner",
250
246
  issueId,
@@ -260,12 +256,16 @@ export class DagRunner {
260
256
  payload: { root_id: rootId, step },
261
257
  });
262
258
  await this.#store.claim(issueId);
259
+ // Track attempt count for circuit breaker.
260
+ const attempt = (this.#attempts.get(issueId) ?? 0) + 1;
261
+ this.#attempts.set(issueId, attempt);
263
262
  // 4. Route + 5. Render + 6. Execute.
264
- const cfg = await this.#resolveConfig(issue);
265
- const logSuffix = "";
263
+ const cfg = await this.#resolveConfig();
264
+ const logSuffix = attempt > 1 ? `attempt-${attempt}` : "";
266
265
  const onBackendLine = hooks?.onBackendLine;
267
266
  const { exitCode, elapsedS } = await this.#executeBackend(issue, cfg, rootId, step, {
268
267
  logSuffix,
268
+ attempt,
269
269
  onLine: onBackendLine
270
270
  ? (line) => onBackendLine({ rootId, step, issueId, logSuffix, line })
271
271
  : undefined,
@@ -277,6 +277,11 @@ export class DagRunner {
277
277
  return final;
278
278
  }
279
279
  if (updated.status !== "closed") {
280
+ await this.#events.emit("dag.step.force_close", {
281
+ source: "dag_runner",
282
+ issueId,
283
+ payload: { root_id: rootId, step, role, attempt, reason: "agent_did_not_close" },
284
+ });
280
285
  updated = await this.#store.close(issueId, "failure");
281
286
  }
282
287
  // 8. Log to forum.
@@ -309,9 +314,18 @@ export class DagRunner {
309
314
  outcome: updated.outcome,
310
315
  },
311
316
  });
312
- // 9. Re-orchestrate on failure / needs_work.
317
+ // 9. Re-orchestrate on failure / needs_work (circuit breaker: max 3 attempts).
313
318
  if (updated.outcome && this.#reorchestrateOutcomes.has(updated.outcome)) {
314
- await this.#reopenForOrchestration(issueId, { reason: `outcome=${updated.outcome}`, step });
319
+ if (attempt < 3) {
320
+ await this.#reopenForOrchestration(issueId, { reason: `outcome=${updated.outcome}`, step });
321
+ }
322
+ else {
323
+ await this.#events.emit("dag.circuit_breaker", {
324
+ source: "dag_runner",
325
+ issueId,
326
+ payload: { root_id: rootId, step, attempt, outcome: updated.outcome },
327
+ });
328
+ }
315
329
  }
316
330
  }
317
331
  final = { status: "max_steps_exhausted", steps: maxSteps, error: "" };
@@ -1,4 +1,5 @@
1
1
  export type MuRole = "orchestrator" | "worker";
2
- export declare function parseMuRole(role: string | null | undefined): MuRole;
2
+ /** Determine role from tags. Defaults to orchestrator if no role tag present. */
3
+ export declare function roleFromTags(tags: readonly string[]): MuRole;
3
4
  export declare function systemPromptForRole(role: MuRole): string;
4
5
  //# sourceMappingURL=mu_roles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mu_roles.d.ts","sourceRoot":"","sources":["../src/mu_roles.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE/C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAanE;AA2DD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA2DxD"}
1
+ {"version":3,"file":"mu_roles.d.ts","sourceRoot":"","sources":["../src/mu_roles.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE/C,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM5D;AA2DD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuExD"}
package/dist/mu_roles.js CHANGED
@@ -1,12 +1,12 @@
1
- export function parseMuRole(role) {
2
- if (role == null) {
3
- return "orchestrator";
1
+ /** Determine role from tags. Defaults to orchestrator if no role tag present. */
2
+ export function roleFromTags(tags) {
3
+ for (const tag of tags) {
4
+ if (tag === "role:worker")
5
+ return "worker";
6
+ if (tag === "role:orchestrator")
7
+ return "orchestrator";
4
8
  }
5
- const trimmed = role.trim();
6
- if (trimmed === "orchestrator" || trimmed === "worker") {
7
- return trimmed;
8
- }
9
- throw new Error(`unsupported execution_spec.role=${JSON.stringify(trimmed)} (only "orchestrator" and "worker" are supported)`);
9
+ return "orchestrator";
10
10
  }
11
11
  /* ------------------------------------------------------------------ */
12
12
  /* mu CLI reference */
@@ -90,6 +90,18 @@ export function systemPromptForRole(role) {
90
90
  "- Assign executable leaves to role=worker.",
91
91
  "- If a child requires further decomposition, assign role=orchestrator.",
92
92
  "",
93
+ "## Review Pattern",
94
+ "",
95
+ "For work that benefits from verification, create a review issue:",
96
+ "1. Create worker issues for implementation.",
97
+ "2. Create a review issue (`--role orchestrator`) blocked by the workers.",
98
+ "3. The review runs after workers complete — verify, test, or expand further.",
99
+ "",
100
+ "Example:",
101
+ ' mu issues create "Implement X" --parent <id> --role worker',
102
+ ' mu issues create "Review X" --parent <id> --role orchestrator',
103
+ " mu issues dep <worker-id> blocks <review-id>",
104
+ "",
93
105
  MU_CLI_REFERENCE,
94
106
  ].join("\n");
95
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-orchestrator",
3
- "version": "26.2.19",
3
+ "version": "26.2.21",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,9 +14,9 @@
14
14
  "dist/**"
15
15
  ],
16
16
  "dependencies": {
17
- "@femtomc/mu-core": "26.2.19",
18
- "@femtomc/mu-forum": "26.2.19",
19
- "@femtomc/mu-issue": "26.2.19",
17
+ "@femtomc/mu-core": "26.2.21",
18
+ "@femtomc/mu-forum": "26.2.21",
19
+ "@femtomc/mu-issue": "26.2.21",
20
20
  "@mariozechner/pi-agent-core": "^0.52.12",
21
21
  "@mariozechner/pi-coding-agent": "^0.52.12",
22
22
  "@mariozechner/pi-ai": "^0.52.12"