@useorgx/openclaw-plugin 0.4.1 → 0.4.4

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.
@@ -30,6 +30,7 @@ import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from
30
30
  import { readByokKeys, writeByokKeys } from "./byok-store.js";
31
31
  import { computeMilestoneRollup, computeWorkstreamRollup, } from "./reporting/rollups.js";
32
32
  import { listRuntimeInstances, resolveRuntimeHookToken, upsertRuntimeInstanceFromHook, } from "./runtime-instance-store.js";
33
+ import { readOpenClawSettingsSnapshot, resolvePreferredOpenClawProvider, } from "./openclaw-settings.js";
33
34
  // =============================================================================
34
35
  // Helpers
35
36
  // =============================================================================
@@ -203,48 +204,60 @@ async function setOpenClawAgentModel(input) {
203
204
  }
204
205
  }
205
206
  async function listOpenClawProviderModels(input) {
206
- const result = await runCommandCollect({
207
- command: "openclaw",
208
- args: [
209
- "models",
210
- "--agent",
211
- input.agentId,
212
- "list",
213
- "--provider",
214
- input.provider,
215
- "--json",
216
- ],
217
- timeoutMs: 10_000,
218
- env: resolveByokEnvOverrides(),
219
- });
220
- if (result.exitCode !== 0) {
221
- throw new Error(result.stderr.trim() || "openclaw models list failed");
222
- }
223
- const parsed = parseJsonSafe(result.stdout);
224
- if (!parsed || typeof parsed !== "object") {
225
- const trimmed = result.stdout.trim();
226
- if (!trimmed || /no models found/i.test(trimmed)) {
227
- return [];
228
- }
229
- throw new Error("openclaw models list returned invalid JSON");
230
- }
231
- const modelsRaw = "models" in parsed && Array.isArray(parsed.models)
232
- ? parsed.models
233
- : [];
234
- return modelsRaw
235
- .map((entry) => {
236
- if (!entry || typeof entry !== "object")
237
- return null;
238
- const row = entry;
239
- const key = typeof row.key === "string" ? row.key.trim() : "";
240
- const tags = Array.isArray(row.tags)
241
- ? row.tags.filter((t) => typeof t === "string")
207
+ const providerArgs = input.provider === "openai" ? ["openai-codex", "openai"] : [input.provider];
208
+ let lastError = null;
209
+ for (const providerArg of providerArgs) {
210
+ const result = await runCommandCollect({
211
+ command: "openclaw",
212
+ args: [
213
+ "models",
214
+ "--agent",
215
+ input.agentId,
216
+ "list",
217
+ "--provider",
218
+ providerArg,
219
+ "--json",
220
+ ],
221
+ timeoutMs: 10_000,
222
+ env: resolveByokEnvOverrides(),
223
+ });
224
+ if (result.exitCode !== 0) {
225
+ lastError = new Error(result.stderr.trim() || "openclaw models list failed");
226
+ continue;
227
+ }
228
+ const parsed = parseJsonSafe(result.stdout);
229
+ if (!parsed || typeof parsed !== "object") {
230
+ const trimmed = result.stdout.trim();
231
+ if (!trimmed || /no models found/i.test(trimmed)) {
232
+ if (providerArg === providerArgs[providerArgs.length - 1])
233
+ return [];
234
+ continue;
235
+ }
236
+ lastError = new Error("openclaw models list returned invalid JSON");
237
+ continue;
238
+ }
239
+ const modelsRaw = "models" in parsed && Array.isArray(parsed.models)
240
+ ? parsed.models
242
241
  : [];
243
- if (!key)
244
- return null;
245
- return { key, tags };
246
- })
247
- .filter((entry) => Boolean(entry));
242
+ const models = modelsRaw
243
+ .map((entry) => {
244
+ if (!entry || typeof entry !== "object")
245
+ return null;
246
+ const row = entry;
247
+ const key = typeof row.key === "string" ? row.key.trim() : "";
248
+ const tags = Array.isArray(row.tags)
249
+ ? row.tags.filter((t) => typeof t === "string")
250
+ : [];
251
+ if (!key)
252
+ return null;
253
+ return { key, tags };
254
+ })
255
+ .filter((entry) => Boolean(entry));
256
+ if (models.length > 0 || providerArg === providerArgs[providerArgs.length - 1]) {
257
+ return models;
258
+ }
259
+ }
260
+ throw lastError ?? new Error("openclaw models list failed");
248
261
  }
249
262
  function pickPreferredModel(models) {
250
263
  if (models.length === 0)
@@ -256,7 +269,7 @@ async function configureOpenClawProviderRouting(input) {
256
269
  const requestedModel = (input.requestedModel ?? "").trim() || null;
257
270
  // Fast path: use known aliases where possible.
258
271
  const aliasByProvider = {
259
- anthropic: "opus",
272
+ anthropic: "sonnet",
260
273
  openrouter: "sonnet",
261
274
  openai: null,
262
275
  };
@@ -281,6 +294,18 @@ async function configureOpenClawProviderRouting(input) {
281
294
  await setOpenClawAgentModel({ agentId: input.agentId, model: selected });
282
295
  return { provider: input.provider, model: selected };
283
296
  }
297
+ function resolveAutoOpenClawProvider() {
298
+ try {
299
+ const settings = readOpenClawSettingsSnapshot();
300
+ const provider = resolvePreferredOpenClawProvider(settings.raw);
301
+ if (!provider)
302
+ return null;
303
+ return provider;
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ }
284
309
  function isPidAlive(pid) {
285
310
  if (!Number.isFinite(pid) || pid <= 0)
286
311
  return false;
@@ -1234,6 +1259,86 @@ function idempotencyKey(parts) {
1234
1259
  const suffix = stableHash(raw).slice(0, 20);
1235
1260
  return `${cleaned}:${suffix}`.slice(0, 120);
1236
1261
  }
1262
+ const ORGX_SKILL_BY_DOMAIN = {
1263
+ engineering: "orgx-engineering-agent",
1264
+ product: "orgx-product-agent",
1265
+ marketing: "orgx-marketing-agent",
1266
+ sales: "orgx-sales-agent",
1267
+ operations: "orgx-operations-agent",
1268
+ design: "orgx-design-agent",
1269
+ orchestration: "orgx-orchestrator-agent",
1270
+ };
1271
+ function normalizeExecutionDomain(value) {
1272
+ const raw = (value ?? "").trim().toLowerCase();
1273
+ if (!raw)
1274
+ return null;
1275
+ if (raw === "orchestrator")
1276
+ return "orchestration";
1277
+ if (raw === "ops")
1278
+ return "operations";
1279
+ return Object.prototype.hasOwnProperty.call(ORGX_SKILL_BY_DOMAIN, raw)
1280
+ ? raw
1281
+ : null;
1282
+ }
1283
+ function inferExecutionDomainFromText(...values) {
1284
+ const text = values
1285
+ .map((value) => (value ?? "").trim().toLowerCase())
1286
+ .filter((value) => value.length > 0)
1287
+ .join(" ");
1288
+ if (!text)
1289
+ return "engineering";
1290
+ if (/\b(marketing|campaign|copy|ad|content)\b/.test(text))
1291
+ return "marketing";
1292
+ if (/\b(sales|meddic|pipeline|deal|outreach)\b/.test(text))
1293
+ return "sales";
1294
+ if (/\b(design|ui|ux|brand|wcag)\b/.test(text))
1295
+ return "design";
1296
+ if (/\b(product|prd|roadmap|prioritization)\b/.test(text))
1297
+ return "product";
1298
+ if (/\b(ops|operations|incident|reliability|oncall|slo)\b/.test(text))
1299
+ return "operations";
1300
+ if (/\b(orchestration|dispatch|handoff)\b/.test(text))
1301
+ return "orchestration";
1302
+ return "engineering";
1303
+ }
1304
+ function deriveExecutionPolicy(taskNode, workstreamNode) {
1305
+ const domainCandidate = taskNode.assignedAgents
1306
+ .map((agent) => normalizeExecutionDomain(agent.domain))
1307
+ .find((domain) => Boolean(domain)) ??
1308
+ (workstreamNode
1309
+ ? workstreamNode.assignedAgents
1310
+ .map((agent) => normalizeExecutionDomain(agent.domain))
1311
+ .find((domain) => Boolean(domain))
1312
+ : null) ??
1313
+ inferExecutionDomainFromText(taskNode.title, workstreamNode?.title ?? null);
1314
+ const domain = normalizeExecutionDomain(domainCandidate) ?? "engineering";
1315
+ const requiredSkill = ORGX_SKILL_BY_DOMAIN[domain] ?? ORGX_SKILL_BY_DOMAIN.engineering;
1316
+ return { domain, requiredSkills: [requiredSkill] };
1317
+ }
1318
+ function spawnGuardIsRateLimited(result) {
1319
+ if (!result || typeof result !== "object")
1320
+ return false;
1321
+ const record = result;
1322
+ const checks = record.checks;
1323
+ if (!checks || typeof checks !== "object")
1324
+ return false;
1325
+ const rateLimit = checks.rateLimit;
1326
+ if (!rateLimit || typeof rateLimit !== "object")
1327
+ return false;
1328
+ return rateLimit.passed === false;
1329
+ }
1330
+ function summarizeSpawnGuardBlockReason(result) {
1331
+ if (!result || typeof result !== "object")
1332
+ return "Spawn guard denied dispatch.";
1333
+ const record = result;
1334
+ const blockedReason = pickString(record, ["blockedReason", "blocked_reason"]);
1335
+ if (blockedReason)
1336
+ return blockedReason;
1337
+ if (spawnGuardIsRateLimited(result)) {
1338
+ return "Spawn guard rate limit reached.";
1339
+ }
1340
+ return "Spawn guard denied dispatch.";
1341
+ }
1237
1342
  const DEFAULT_DURATION_HOURS = {
1238
1343
  initiative: 40,
1239
1344
  workstream: 16,
@@ -2125,6 +2230,263 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2125
2230
  }
2126
2231
  }
2127
2232
  }
2233
+ async function requestDecisionSafe(input) {
2234
+ const initiativeId = input.initiativeId?.trim() ?? "";
2235
+ const title = input.title.trim();
2236
+ if (!initiativeId || !title)
2237
+ return;
2238
+ try {
2239
+ await client.applyChangeset({
2240
+ initiative_id: initiativeId,
2241
+ correlation_id: input.correlationId?.trim() || undefined,
2242
+ source_client: "openclaw",
2243
+ idempotency_key: idempotencyKey([
2244
+ "openclaw",
2245
+ "decision",
2246
+ initiativeId,
2247
+ title,
2248
+ input.correlationId ?? null,
2249
+ ]),
2250
+ operations: [
2251
+ {
2252
+ op: "decision.create",
2253
+ title,
2254
+ summary: input.summary ?? undefined,
2255
+ urgency: input.urgency ?? "high",
2256
+ options: input.options ?? [],
2257
+ blocking: input.blocking ?? true,
2258
+ },
2259
+ ],
2260
+ });
2261
+ }
2262
+ catch {
2263
+ // best effort
2264
+ }
2265
+ }
2266
+ async function checkSpawnGuardSafe(input) {
2267
+ const scopedClient = client;
2268
+ if (typeof scopedClient.checkSpawnGuard !== "function") {
2269
+ return null;
2270
+ }
2271
+ const taskId = input.taskId?.trim() ?? "";
2272
+ const targetLabel = input.targetLabel?.trim() ||
2273
+ (taskId ? `task ${taskId}` : "dispatch target");
2274
+ try {
2275
+ return await scopedClient.checkSpawnGuard(input.domain, taskId || undefined);
2276
+ }
2277
+ catch (err) {
2278
+ await emitActivitySafe({
2279
+ initiativeId: input.initiativeId,
2280
+ correlationId: input.correlationId,
2281
+ phase: "blocked",
2282
+ level: "warn",
2283
+ message: `Spawn guard check degraded for ${targetLabel}; continuing with local policy.`,
2284
+ metadata: {
2285
+ event: "spawn_guard_degraded",
2286
+ task_id: taskId || null,
2287
+ domain: input.domain,
2288
+ error: safeErrorMessage(err),
2289
+ },
2290
+ });
2291
+ return null;
2292
+ }
2293
+ }
2294
+ function extractSpawnGuardModelTier(result) {
2295
+ if (!result || typeof result !== "object")
2296
+ return null;
2297
+ return (pickString(result, ["modelTier", "model_tier"]) ??
2298
+ null);
2299
+ }
2300
+ function formatRequiredSkills(requiredSkills) {
2301
+ const normalized = requiredSkills
2302
+ .map((entry) => entry.trim())
2303
+ .filter((entry) => entry.length > 0)
2304
+ .map((entry) => (entry.startsWith("$") ? entry : `$${entry}`));
2305
+ return normalized.length > 0
2306
+ ? normalized.join(", ")
2307
+ : "$orgx-engineering-agent";
2308
+ }
2309
+ function buildPolicyEnforcedMessage(input) {
2310
+ const modelTier = extractSpawnGuardModelTier(input.spawnGuardResult ?? null);
2311
+ return [
2312
+ `Execution policy: ${input.executionPolicy.domain}`,
2313
+ `Required skills: ${formatRequiredSkills(input.executionPolicy.requiredSkills)}`,
2314
+ modelTier ? `Spawn guard model tier: ${modelTier}` : null,
2315
+ "",
2316
+ input.baseMessage,
2317
+ ]
2318
+ .filter((entry) => Boolean(entry))
2319
+ .join("\n");
2320
+ }
2321
+ async function resolveDispatchExecutionPolicy(input) {
2322
+ const initiativeId = input.initiativeId?.trim() ?? "";
2323
+ const taskId = input.taskId?.trim() ?? "";
2324
+ const workstreamId = input.workstreamId?.trim() ?? "";
2325
+ let resolvedTaskTitle = input.taskTitle?.trim() || null;
2326
+ let resolvedWorkstreamTitle = input.workstreamTitle?.trim() || null;
2327
+ if (initiativeId && (taskId || workstreamId)) {
2328
+ try {
2329
+ const graph = await buildMissionControlGraph(client, initiativeId);
2330
+ const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
2331
+ const taskNode = taskId ? nodeById.get(taskId) ?? null : null;
2332
+ const workstreamNode = workstreamId ? nodeById.get(workstreamId) ?? null : null;
2333
+ if (taskNode && taskNode.type === "task") {
2334
+ resolvedTaskTitle = resolvedTaskTitle ?? taskNode.title;
2335
+ const relatedWorkstream = (taskNode.workstreamId ? nodeById.get(taskNode.workstreamId) ?? null : null) ??
2336
+ workstreamNode;
2337
+ const normalizedWorkstream = relatedWorkstream && relatedWorkstream.type === "workstream"
2338
+ ? relatedWorkstream
2339
+ : null;
2340
+ resolvedWorkstreamTitle =
2341
+ resolvedWorkstreamTitle ?? normalizedWorkstream?.title ?? null;
2342
+ return {
2343
+ executionPolicy: deriveExecutionPolicy(taskNode, normalizedWorkstream),
2344
+ taskTitle: resolvedTaskTitle,
2345
+ workstreamTitle: resolvedWorkstreamTitle,
2346
+ };
2347
+ }
2348
+ if (workstreamNode && workstreamNode.type === "workstream") {
2349
+ resolvedWorkstreamTitle = resolvedWorkstreamTitle ?? workstreamNode.title;
2350
+ const assignedDomain = workstreamNode.assignedAgents
2351
+ .map((agent) => normalizeExecutionDomain(agent.domain))
2352
+ .find((entry) => Boolean(entry));
2353
+ const domain = assignedDomain ??
2354
+ inferExecutionDomainFromText(workstreamNode.title, input.initiativeTitle, input.message);
2355
+ const normalizedDomain = normalizeExecutionDomain(domain) ?? "engineering";
2356
+ return {
2357
+ executionPolicy: {
2358
+ domain: normalizedDomain,
2359
+ requiredSkills: [
2360
+ ORGX_SKILL_BY_DOMAIN[normalizedDomain] ??
2361
+ ORGX_SKILL_BY_DOMAIN.engineering,
2362
+ ],
2363
+ },
2364
+ taskTitle: resolvedTaskTitle,
2365
+ workstreamTitle: resolvedWorkstreamTitle,
2366
+ };
2367
+ }
2368
+ }
2369
+ catch {
2370
+ // best effort
2371
+ }
2372
+ }
2373
+ const inferredDomain = normalizeExecutionDomain(inferExecutionDomainFromText(resolvedTaskTitle, resolvedWorkstreamTitle, input.initiativeTitle, input.message)) ?? "engineering";
2374
+ return {
2375
+ executionPolicy: {
2376
+ domain: inferredDomain,
2377
+ requiredSkills: [
2378
+ ORGX_SKILL_BY_DOMAIN[inferredDomain] ?? ORGX_SKILL_BY_DOMAIN.engineering,
2379
+ ],
2380
+ },
2381
+ taskTitle: resolvedTaskTitle,
2382
+ workstreamTitle: resolvedWorkstreamTitle,
2383
+ };
2384
+ }
2385
+ async function enforceSpawnGuardForDispatch(input) {
2386
+ const taskId = input.taskId?.trim() ?? "";
2387
+ const workstreamId = input.workstreamId?.trim() ?? "";
2388
+ const taskTitle = input.taskTitle?.trim() || null;
2389
+ const workstreamTitle = input.workstreamTitle?.trim() || null;
2390
+ const targetLabel = taskId
2391
+ ? `task ${taskTitle ?? taskId}`
2392
+ : workstreamId
2393
+ ? `workstream ${workstreamTitle ?? workstreamId}`
2394
+ : "dispatch target";
2395
+ const spawnGuardResult = await checkSpawnGuardSafe({
2396
+ domain: input.executionPolicy.domain,
2397
+ taskId: taskId || workstreamId || null,
2398
+ initiativeId: input.initiativeId,
2399
+ correlationId: input.correlationId,
2400
+ targetLabel,
2401
+ });
2402
+ if (!spawnGuardResult || typeof spawnGuardResult !== "object") {
2403
+ return {
2404
+ allowed: true,
2405
+ retryable: false,
2406
+ blockedReason: null,
2407
+ spawnGuardResult,
2408
+ };
2409
+ }
2410
+ const allowed = spawnGuardResult.allowed;
2411
+ if (allowed !== false) {
2412
+ return {
2413
+ allowed: true,
2414
+ retryable: false,
2415
+ blockedReason: null,
2416
+ spawnGuardResult,
2417
+ };
2418
+ }
2419
+ const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
2420
+ const retryable = spawnGuardIsRateLimited(spawnGuardResult);
2421
+ const blockedEvent = retryable
2422
+ ? `${input.sourceEventPrefix}_spawn_guard_rate_limited`
2423
+ : `${input.sourceEventPrefix}_spawn_guard_blocked`;
2424
+ await emitActivitySafe({
2425
+ initiativeId: input.initiativeId,
2426
+ correlationId: input.correlationId,
2427
+ phase: "blocked",
2428
+ level: retryable ? "warn" : "error",
2429
+ message: retryable
2430
+ ? `Spawn guard rate-limited ${targetLabel}; deferring launch.`
2431
+ : `Spawn guard blocked ${targetLabel}.`,
2432
+ metadata: {
2433
+ event: blockedEvent,
2434
+ agent_id: input.agentId ?? null,
2435
+ task_id: taskId || null,
2436
+ task_title: taskTitle,
2437
+ workstream_id: workstreamId || null,
2438
+ workstream_title: workstreamTitle,
2439
+ domain: input.executionPolicy.domain,
2440
+ required_skills: input.executionPolicy.requiredSkills,
2441
+ blocked_reason: blockedReason,
2442
+ spawn_guard: spawnGuardResult,
2443
+ },
2444
+ nextStep: retryable
2445
+ ? "Retry dispatch when spawn rate limits recover."
2446
+ : "Review decision and unblock guard checks before retry.",
2447
+ });
2448
+ if (!retryable && input.initiativeId && taskId) {
2449
+ try {
2450
+ await client.updateEntity("task", taskId, { status: "blocked" });
2451
+ }
2452
+ catch {
2453
+ // best effort
2454
+ }
2455
+ await syncParentRollupsForTask({
2456
+ initiativeId: input.initiativeId,
2457
+ taskId,
2458
+ workstreamId: workstreamId || null,
2459
+ milestoneId: input.milestoneId ?? null,
2460
+ correlationId: input.correlationId,
2461
+ });
2462
+ }
2463
+ if (!retryable) {
2464
+ await requestDecisionSafe({
2465
+ initiativeId: input.initiativeId,
2466
+ correlationId: input.correlationId,
2467
+ title: `Unblock ${targetLabel}`,
2468
+ summary: [
2469
+ `${targetLabel} failed spawn guard checks.`,
2470
+ `Reason: ${blockedReason}`,
2471
+ `Domain: ${input.executionPolicy.domain}`,
2472
+ `Required skills: ${input.executionPolicy.requiredSkills.join(", ")}`,
2473
+ ].join(" "),
2474
+ urgency: "high",
2475
+ options: [
2476
+ "Approve exception and continue",
2477
+ "Reassign task/domain",
2478
+ "Pause and investigate quality gate",
2479
+ ],
2480
+ blocking: true,
2481
+ });
2482
+ }
2483
+ return {
2484
+ allowed: false,
2485
+ retryable,
2486
+ blockedReason,
2487
+ spawnGuardResult,
2488
+ };
2489
+ }
2128
2490
  async function syncParentRollupsForTask(input) {
2129
2491
  const initiativeId = input.initiativeId?.trim() ?? "";
2130
2492
  const taskId = input.taskId?.trim() ?? "";
@@ -2464,25 +2826,61 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2464
2826
  async function dispatchFallbackWorkstreamTurn(input) {
2465
2827
  const now = new Date().toISOString();
2466
2828
  const sessionId = randomUUID();
2467
- const message = [
2829
+ const policyResolution = await resolveDispatchExecutionPolicy({
2830
+ initiativeId: input.initiativeId,
2831
+ initiativeTitle: input.initiativeTitle,
2832
+ workstreamId: input.workstreamId,
2833
+ workstreamTitle: input.workstreamTitle,
2834
+ message: "Continue this workstream from the latest context. Identify and execute the next concrete task.",
2835
+ });
2836
+ const executionPolicy = policyResolution.executionPolicy;
2837
+ const resolvedWorkstreamTitle = policyResolution.workstreamTitle ?? input.workstreamTitle;
2838
+ const guard = await enforceSpawnGuardForDispatch({
2839
+ sourceEventPrefix: "next_up_fallback",
2840
+ initiativeId: input.initiativeId,
2841
+ correlationId: sessionId,
2842
+ executionPolicy,
2843
+ agentId: input.agentId,
2844
+ workstreamId: input.workstreamId,
2845
+ workstreamTitle: resolvedWorkstreamTitle,
2846
+ });
2847
+ if (!guard.allowed) {
2848
+ return {
2849
+ sessionId: null,
2850
+ pid: null,
2851
+ blockedReason: guard.blockedReason,
2852
+ retryable: guard.retryable,
2853
+ executionPolicy,
2854
+ spawnGuardResult: guard.spawnGuardResult,
2855
+ };
2856
+ }
2857
+ const baseMessage = [
2468
2858
  `Initiative: ${input.initiativeTitle}`,
2469
- `Workstream: ${input.workstreamTitle}`,
2859
+ `Workstream: ${resolvedWorkstreamTitle}`,
2470
2860
  "",
2471
2861
  "Continue this workstream from the latest context.",
2472
2862
  "Identify and execute the next concrete task, then provide a concise progress summary.",
2473
2863
  ].join("\n");
2864
+ const message = buildPolicyEnforcedMessage({
2865
+ baseMessage,
2866
+ executionPolicy,
2867
+ spawnGuardResult: guard.spawnGuardResult,
2868
+ });
2474
2869
  await emitActivitySafe({
2475
2870
  initiativeId: input.initiativeId,
2476
2871
  correlationId: sessionId,
2477
2872
  phase: "execution",
2478
2873
  level: "info",
2479
- message: `Next Up dispatched ${input.workstreamTitle}.`,
2874
+ message: `Next Up dispatched ${resolvedWorkstreamTitle}.`,
2480
2875
  metadata: {
2481
2876
  event: "next_up_manual_dispatch_started",
2482
2877
  agent_id: input.agentId,
2483
2878
  session_id: sessionId,
2484
2879
  workstream_id: input.workstreamId,
2485
- workstream_title: input.workstreamTitle,
2880
+ workstream_title: resolvedWorkstreamTitle,
2881
+ domain: executionPolicy.domain,
2882
+ required_skills: executionPolicy.requiredSkills,
2883
+ spawn_guard_model_tier: extractSpawnGuardModelTier(guard.spawnGuardResult),
2486
2884
  fallback: true,
2487
2885
  },
2488
2886
  });
@@ -2512,7 +2910,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2512
2910
  startedAt: now,
2513
2911
  status: "running",
2514
2912
  });
2515
- return { sessionId, pid: spawned.pid };
2913
+ return {
2914
+ sessionId,
2915
+ pid: spawned.pid,
2916
+ blockedReason: null,
2917
+ retryable: false,
2918
+ executionPolicy,
2919
+ spawnGuardResult: guard.spawnGuardResult,
2920
+ };
2516
2921
  }
2517
2922
  async function tickAutoContinueRun(run) {
2518
2923
  if (run.status !== "running" && run.status !== "stopping")
@@ -2579,6 +2984,27 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2579
2984
  error_message: summary.errorMessage,
2580
2985
  },
2581
2986
  });
2987
+ if (summary.hadError && record.taskId) {
2988
+ await requestDecisionSafe({
2989
+ initiativeId: run.initiativeId,
2990
+ correlationId: record.runId,
2991
+ title: `Unblock auto-continue task ${record.taskId}`,
2992
+ summary: [
2993
+ `Task ${record.taskId} finished with runtime error in session ${record.runId}.`,
2994
+ summary.errorMessage ? `Error: ${summary.errorMessage}` : null,
2995
+ `Workstream: ${record.workstreamId ?? "unknown"}.`,
2996
+ ]
2997
+ .filter((line) => Boolean(line))
2998
+ .join(" "),
2999
+ urgency: "high",
3000
+ options: [
3001
+ "Retry task in auto-continue",
3002
+ "Assign manual recovery owner",
3003
+ "Pause initiative until fixed",
3004
+ ],
3005
+ blocking: true,
3006
+ });
3007
+ }
2582
3008
  run.lastRunId = record.runId;
2583
3009
  run.lastTaskId = record.taskId ?? run.lastTaskId;
2584
3010
  run.activeRunId = null;
@@ -2701,30 +3127,120 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2701
3127
  const milestoneTitle = nextTaskNode.milestoneId
2702
3128
  ? nodeById.get(nextTaskNode.milestoneId)?.title ?? null
2703
3129
  : null;
3130
+ const workstreamNode = nextTaskNode.workstreamId
3131
+ ? nodeById.get(nextTaskNode.workstreamId) ?? null
3132
+ : null;
3133
+ const executionPolicy = deriveExecutionPolicy(nextTaskNode, workstreamNode);
3134
+ const spawnGuardResult = await checkSpawnGuardSafe({
3135
+ domain: executionPolicy.domain,
3136
+ taskId: nextTaskNode.id,
3137
+ initiativeId: run.initiativeId,
3138
+ correlationId: sessionId,
3139
+ });
3140
+ if (spawnGuardResult && typeof spawnGuardResult === "object") {
3141
+ const allowed = spawnGuardResult.allowed;
3142
+ if (allowed === false) {
3143
+ const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
3144
+ if (spawnGuardIsRateLimited(spawnGuardResult)) {
3145
+ run.lastError = blockedReason;
3146
+ run.updatedAt = now;
3147
+ await emitActivitySafe({
3148
+ initiativeId: run.initiativeId,
3149
+ correlationId: sessionId,
3150
+ phase: "blocked",
3151
+ level: "warn",
3152
+ message: `Spawn guard rate-limited task ${nextTaskNode.id}; waiting to retry.`,
3153
+ metadata: {
3154
+ event: "auto_continue_spawn_guard_rate_limited",
3155
+ task_id: nextTaskNode.id,
3156
+ task_title: nextTaskNode.title,
3157
+ domain: executionPolicy.domain,
3158
+ required_skills: executionPolicy.requiredSkills,
3159
+ spawn_guard: spawnGuardResult,
3160
+ },
3161
+ });
3162
+ return;
3163
+ }
3164
+ try {
3165
+ await client.updateEntity("task", nextTaskNode.id, {
3166
+ status: "blocked",
3167
+ });
3168
+ }
3169
+ catch {
3170
+ // best effort
3171
+ }
3172
+ await syncParentRollupsForTask({
3173
+ initiativeId: run.initiativeId,
3174
+ taskId: nextTaskNode.id,
3175
+ workstreamId: nextTaskNode.workstreamId,
3176
+ milestoneId: nextTaskNode.milestoneId,
3177
+ correlationId: sessionId,
3178
+ });
3179
+ await emitActivitySafe({
3180
+ initiativeId: run.initiativeId,
3181
+ correlationId: sessionId,
3182
+ phase: "blocked",
3183
+ level: "error",
3184
+ message: `Auto-continue blocked by spawn guard on task ${nextTaskNode.id}.`,
3185
+ metadata: {
3186
+ event: "auto_continue_spawn_guard_blocked",
3187
+ task_id: nextTaskNode.id,
3188
+ task_title: nextTaskNode.title,
3189
+ domain: executionPolicy.domain,
3190
+ required_skills: executionPolicy.requiredSkills,
3191
+ blocked_reason: blockedReason,
3192
+ spawn_guard: spawnGuardResult,
3193
+ },
3194
+ });
3195
+ await requestDecisionSafe({
3196
+ initiativeId: run.initiativeId,
3197
+ correlationId: sessionId,
3198
+ title: `Unblock auto-continue task ${nextTaskNode.title}`,
3199
+ summary: [
3200
+ `Task ${nextTaskNode.id} failed spawn guard checks.`,
3201
+ `Reason: ${blockedReason}`,
3202
+ `Domain: ${executionPolicy.domain}`,
3203
+ `Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
3204
+ ].join(" "),
3205
+ urgency: "high",
3206
+ options: [
3207
+ "Approve exception and continue",
3208
+ "Reassign task/domain",
3209
+ "Pause and investigate quality gate",
3210
+ ],
3211
+ blocking: true,
3212
+ });
3213
+ await stopAutoContinueRun({
3214
+ run,
3215
+ reason: "blocked",
3216
+ error: blockedReason,
3217
+ });
3218
+ return;
3219
+ }
3220
+ }
2704
3221
  const message = [
2705
3222
  initiativeNode ? `Initiative: ${initiativeNode.title}` : null,
2706
3223
  workstreamTitle ? `Workstream: ${workstreamTitle}` : null,
2707
3224
  milestoneTitle ? `Milestone: ${milestoneTitle}` : null,
2708
3225
  "",
2709
3226
  `Task: ${nextTaskNode.title}`,
3227
+ `Execution policy: ${executionPolicy.domain}`,
3228
+ `Required skills: ${executionPolicy.requiredSkills.map((skill) => `$${skill}`).join(", ")}`,
2710
3229
  "",
2711
3230
  "Execute this task. When finished, provide a concise completion summary and any relevant commands/notes.",
2712
3231
  ]
2713
3232
  .filter((line) => typeof line === "string")
2714
3233
  .join("\n");
2715
- if (nextTaskNode.workstreamId) {
2716
- const workstreamNode = nodeById.get(nextTaskNode.workstreamId);
2717
- if (workstreamNode &&
2718
- !isInProgressStatus(workstreamNode.status) &&
2719
- isDispatchableWorkstreamStatus(workstreamNode.status)) {
2720
- try {
2721
- await client.updateEntity("workstream", workstreamNode.id, {
2722
- status: "active",
2723
- });
2724
- }
2725
- catch {
2726
- // best effort
2727
- }
3234
+ if (workstreamNode &&
3235
+ !isInProgressStatus(workstreamNode.status) &&
3236
+ isDispatchableWorkstreamStatus(workstreamNode.status)) {
3237
+ try {
3238
+ await client.updateEntity("workstream", workstreamNode.id, {
3239
+ status: "active",
3240
+ });
3241
+ }
3242
+ catch {
3243
+ // best effort
2728
3244
  }
2729
3245
  }
2730
3246
  try {
@@ -2763,6 +3279,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2763
3279
  workstream_title: workstreamTitle,
2764
3280
  milestone_id: nextTaskNode.milestoneId,
2765
3281
  milestone_title: milestoneTitle,
3282
+ domain: executionPolicy.domain,
3283
+ required_skills: executionPolicy.requiredSkills,
3284
+ spawn_guard_model_tier: spawnGuardResult && typeof spawnGuardResult === "object"
3285
+ ? pickString(spawnGuardResult, ["modelTier", "model_tier"]) ?? null
3286
+ : null,
2766
3287
  },
2767
3288
  });
2768
3289
  upsertAgentContext({
@@ -3580,6 +4101,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3580
4101
  searchParams.get("model_id") ??
3581
4102
  "")
3582
4103
  .trim() || null;
4104
+ const routingProvider = provider ?? (!provider && !requestedModel ? resolveAutoOpenClawProvider() : null);
3583
4105
  const dryRunRaw = payload.dryRun ??
3584
4106
  payload.dry_run ??
3585
4107
  searchParams.get("dryRun") ??
@@ -3588,7 +4110,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3588
4110
  const dryRun = typeof dryRunRaw === "boolean"
3589
4111
  ? dryRunRaw
3590
4112
  : parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null);
3591
- let requiresPremiumLaunch = Boolean(provider) || modelImpliesByok(requestedModel);
4113
+ let requiresPremiumLaunch = Boolean(routingProvider) || modelImpliesByok(requestedModel);
3592
4114
  if (!requiresPremiumLaunch) {
3593
4115
  try {
3594
4116
  const agents = await listAgents();
@@ -3628,12 +4150,23 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3628
4150
  searchParams.get("text") ??
3629
4151
  "")
3630
4152
  .trim();
3631
- const message = messageInput ||
4153
+ const baseMessage = messageInput ||
3632
4154
  (initiativeTitle
3633
4155
  ? `Kick off: ${initiativeTitle}`
3634
4156
  : initiativeId
3635
4157
  ? `Kick off initiative ${initiativeId}`
3636
4158
  : `Kick off agent ${agentId}`);
4159
+ const policyResolution = await resolveDispatchExecutionPolicy({
4160
+ initiativeId,
4161
+ initiativeTitle,
4162
+ workstreamId,
4163
+ taskId,
4164
+ message: baseMessage,
4165
+ });
4166
+ const executionPolicy = policyResolution.executionPolicy;
4167
+ const resolvedTaskTitle = policyResolution.taskTitle;
4168
+ const resolvedWorkstreamTitle = policyResolution.workstreamTitle ??
4169
+ (workstreamId ? `Workstream ${workstreamId}` : null);
3637
4170
  if (dryRun) {
3638
4171
  sendJson(res, 200, {
3639
4172
  ok: true,
@@ -3643,11 +4176,48 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3643
4176
  workstreamId,
3644
4177
  taskId,
3645
4178
  requiresPremiumLaunch,
4179
+ provider: routingProvider,
4180
+ model: requestedModel,
3646
4181
  startedAt: new Date().toISOString(),
3647
- message,
4182
+ message: baseMessage,
4183
+ domain: executionPolicy.domain,
4184
+ requiredSkills: executionPolicy.requiredSkills,
3648
4185
  });
3649
4186
  return true;
3650
4187
  }
4188
+ const guard = await enforceSpawnGuardForDispatch({
4189
+ sourceEventPrefix: "agent_launch",
4190
+ initiativeId,
4191
+ correlationId: sessionId,
4192
+ executionPolicy,
4193
+ agentId,
4194
+ taskId,
4195
+ taskTitle: resolvedTaskTitle,
4196
+ workstreamId,
4197
+ workstreamTitle: resolvedWorkstreamTitle,
4198
+ });
4199
+ if (!guard.allowed) {
4200
+ sendJson(res, guard.retryable ? 429 : 409, {
4201
+ ok: false,
4202
+ code: guard.retryable
4203
+ ? "spawn_guard_rate_limited"
4204
+ : "spawn_guard_blocked",
4205
+ error: guard.blockedReason ??
4206
+ "Spawn guard denied this agent launch.",
4207
+ retryable: guard.retryable,
4208
+ initiativeId,
4209
+ workstreamId,
4210
+ taskId,
4211
+ domain: executionPolicy.domain,
4212
+ requiredSkills: executionPolicy.requiredSkills,
4213
+ });
4214
+ return true;
4215
+ }
4216
+ const message = buildPolicyEnforcedMessage({
4217
+ baseMessage,
4218
+ executionPolicy,
4219
+ spawnGuardResult: guard.spawnGuardResult,
4220
+ });
3651
4221
  if (initiativeId) {
3652
4222
  try {
3653
4223
  await client.updateEntity("initiative", initiativeId, { status: "active" });
@@ -3684,16 +4254,19 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3684
4254
  session_id: sessionId,
3685
4255
  workstream_id: workstreamId,
3686
4256
  task_id: taskId,
3687
- provider,
4257
+ provider: routingProvider,
3688
4258
  model: requestedModel,
4259
+ domain: executionPolicy.domain,
4260
+ required_skills: executionPolicy.requiredSkills,
4261
+ spawn_guard_model_tier: extractSpawnGuardModelTier(guard.spawnGuardResult),
3689
4262
  },
3690
4263
  });
3691
4264
  let routedProvider = null;
3692
4265
  let routedModel = null;
3693
- if (provider) {
4266
+ if (routingProvider) {
3694
4267
  const routed = await configureOpenClawProviderRouting({
3695
4268
  agentId,
3696
- provider,
4269
+ provider: routingProvider,
3697
4270
  requestedModel,
3698
4271
  });
3699
4272
  routedProvider = routed.provider;
@@ -3737,6 +4310,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3737
4310
  workstreamId,
3738
4311
  taskId,
3739
4312
  startedAt: new Date().toISOString(),
4313
+ domain: executionPolicy.domain,
4314
+ requiredSkills: executionPolicy.requiredSkills,
3740
4315
  });
3741
4316
  }
3742
4317
  catch (err) {
@@ -3825,7 +4400,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3825
4400
  record.model ??
3826
4401
  "")
3827
4402
  .trim() || null;
3828
- let requiresPremiumRestart = Boolean(providerOverride) ||
4403
+ const routingProvider = providerOverride ??
4404
+ (!providerOverride && !requestedModel ? resolveAutoOpenClawProvider() : null);
4405
+ let requiresPremiumRestart = Boolean(routingProvider) ||
3829
4406
  modelImpliesByok(requestedModel) ||
3830
4407
  modelImpliesByok(record.model ?? null);
3831
4408
  if (!requiresPremiumRestart) {
@@ -3861,13 +4438,52 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3861
4438
  }
3862
4439
  }
3863
4440
  const sessionId = randomUUID();
3864
- const message = messageOverride ?? record.message ?? `Restart agent ${record.agentId}`;
3865
- let routedProvider = providerOverride ?? null;
4441
+ const baseMessage = messageOverride ?? record.message ?? `Restart agent ${record.agentId}`;
4442
+ const policyResolution = await resolveDispatchExecutionPolicy({
4443
+ initiativeId: record.initiativeId,
4444
+ initiativeTitle: record.initiativeTitle,
4445
+ workstreamId: record.workstreamId,
4446
+ taskId: record.taskId,
4447
+ message: baseMessage,
4448
+ });
4449
+ const executionPolicy = policyResolution.executionPolicy;
4450
+ const guard = await enforceSpawnGuardForDispatch({
4451
+ sourceEventPrefix: "agent_restart",
4452
+ initiativeId: record.initiativeId,
4453
+ correlationId: sessionId,
4454
+ executionPolicy,
4455
+ agentId: record.agentId,
4456
+ taskId: record.taskId,
4457
+ taskTitle: policyResolution.taskTitle,
4458
+ workstreamId: record.workstreamId,
4459
+ workstreamTitle: policyResolution.workstreamTitle,
4460
+ });
4461
+ if (!guard.allowed) {
4462
+ sendJson(res, guard.retryable ? 429 : 409, {
4463
+ ok: false,
4464
+ code: guard.retryable
4465
+ ? "spawn_guard_rate_limited"
4466
+ : "spawn_guard_blocked",
4467
+ error: guard.blockedReason ??
4468
+ "Spawn guard denied this restart.",
4469
+ retryable: guard.retryable,
4470
+ previousRunId,
4471
+ domain: executionPolicy.domain,
4472
+ requiredSkills: executionPolicy.requiredSkills,
4473
+ });
4474
+ return true;
4475
+ }
4476
+ const message = buildPolicyEnforcedMessage({
4477
+ baseMessage,
4478
+ executionPolicy,
4479
+ spawnGuardResult: guard.spawnGuardResult,
4480
+ });
4481
+ let routedProvider = routingProvider ?? null;
3866
4482
  let routedModel = requestedModel ?? null;
3867
- if (providerOverride) {
4483
+ if (routingProvider) {
3868
4484
  const routed = await configureOpenClawProviderRouting({
3869
4485
  agentId: record.agentId,
3870
- provider: providerOverride,
4486
+ provider: routingProvider,
3871
4487
  requestedModel,
3872
4488
  });
3873
4489
  routedProvider = routed.provider;
@@ -3907,6 +4523,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3907
4523
  pid: spawned.pid,
3908
4524
  provider: routedProvider,
3909
4525
  model: routedModel,
4526
+ domain: executionPolicy.domain,
4527
+ requiredSkills: executionPolicy.requiredSkills,
3910
4528
  });
3911
4529
  }
3912
4530
  catch (err) {
@@ -4007,24 +4625,33 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4007
4625
  agentId,
4008
4626
  });
4009
4627
  }
4628
+ const fallbackStarted = Boolean(fallbackDispatch?.sessionId);
4010
4629
  const dispatchMode = run.activeRunId
4011
4630
  ? "task"
4012
- : fallbackDispatch
4631
+ : fallbackStarted
4013
4632
  ? "fallback"
4014
4633
  : "none";
4015
4634
  if (dispatchMode === "none") {
4016
- const reason = run.stopReason === "blocked"
4017
- ? "No dispatchable task is ready for this workstream yet."
4018
- : run.stopReason === "completed"
4019
- ? "No queued task is available for this workstream."
4020
- : "Unable to dispatch this workstream right now.";
4021
- sendJson(res, 409, {
4635
+ const fallbackBlockedReason = fallbackDispatch?.blockedReason ?? null;
4636
+ const reason = fallbackBlockedReason ??
4637
+ (run.stopReason === "blocked"
4638
+ ? "No dispatchable task is ready for this workstream yet."
4639
+ : run.stopReason === "completed"
4640
+ ? "No queued task is available for this workstream."
4641
+ : "Unable to dispatch this workstream right now.");
4642
+ sendJson(res, fallbackDispatch?.retryable ? 429 : 409, {
4022
4643
  ok: false,
4644
+ code: fallbackBlockedReason
4645
+ ? fallbackDispatch?.retryable
4646
+ ? "spawn_guard_rate_limited"
4647
+ : "spawn_guard_blocked"
4648
+ : undefined,
4023
4649
  error: reason,
4024
4650
  run,
4025
4651
  initiativeId,
4026
4652
  workstreamId,
4027
4653
  agentId,
4654
+ fallbackDispatch,
4028
4655
  });
4029
4656
  return true;
4030
4657
  }