@jiggai/recipes 0.4.25 → 0.4.27

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/index.ts CHANGED
@@ -654,10 +654,12 @@ const recipesPlugin = {
654
654
  .description("Claim and execute a single queued workflow run (intended for cron-driven runner)")
655
655
  .requiredOption("--team-id <teamId>", "Team id (workspace-<teamId>)")
656
656
  .option("--lease-seconds <n>", "Lease duration in seconds", (v: string) => Number(v))
657
- .action(async (options: { teamId?: string; leaseSeconds?: number }) => {
657
+ .option("--run-id <runId>", "Only claim this specific run id")
658
+ .action(async (options: { teamId?: string; leaseSeconds?: number; runId?: string }) => {
658
659
  const res = await handleWorkflowsRunnerOnce(api, {
659
660
  teamId: String(options.teamId ?? ""),
660
661
  leaseSeconds: typeof options.leaseSeconds === "number" ? options.leaseSeconds : undefined,
662
+ runId: options.runId,
661
663
  });
662
664
  console.log(JSON.stringify(res, null, 2));
663
665
  });
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.25",
5
+ "version": "0.4.27",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.25",
3
+ "version": "0.4.27",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -17,9 +17,10 @@ export async function handleWorkflowsRun(api: OpenClawPluginApi, opts: {
17
17
  export async function handleWorkflowsRunnerOnce(api: OpenClawPluginApi, opts: {
18
18
  teamId: string;
19
19
  leaseSeconds?: number;
20
+ runId?: string;
20
21
  }) {
21
22
  if (!opts.teamId) throw new Error('--team-id is required');
22
- return runWorkflowRunnerOnce(api, { teamId: opts.teamId, leaseSeconds: opts.leaseSeconds });
23
+ return runWorkflowRunnerOnce(api, { teamId: opts.teamId, leaseSeconds: opts.leaseSeconds, runId: opts.runId });
23
24
  }
24
25
 
25
26
 
@@ -124,6 +124,7 @@ export async function enqueueWorkflowRun(api: OpenClawPluginApi, opts: {
124
124
  export async function runWorkflowRunnerOnce(api: OpenClawPluginApi, opts: {
125
125
  teamId: string;
126
126
  leaseSeconds?: number;
127
+ runId?: string;
127
128
  }) {
128
129
  const teamId = String(opts.teamId);
129
130
  const teamDir = resolveTeamDir(api, teamId);
@@ -169,6 +170,14 @@ export async function runWorkflowRunnerOnce(api: OpenClawPluginApi, opts: {
169
170
  }
170
171
  }
171
172
 
173
+ // If a specific runId was requested, only consider that run.
174
+ const targetRunId = opts.runId?.trim();
175
+ if (targetRunId) {
176
+ const match = candidates.filter((c) => path.basename(path.dirname(c.file)) === targetRunId);
177
+ candidates.length = 0;
178
+ candidates.push(...match);
179
+ }
180
+
172
181
  if (!candidates.length) {
173
182
  return { ok: true as const, teamId, claimed: 0, message: 'No queued runs available.' };
174
183
  }
@@ -19,15 +19,21 @@ import {
19
19
  sanitizeDraftOnlyText, templateReplace,
20
20
  } from './workflow-utils';
21
21
 
22
+ // Max depth for event-driven chaining to prevent runaway recursion.
23
+ const maxChainDepth = 20;
24
+
22
25
  // eslint-disable-next-line complexity, max-lines-per-function
23
26
  export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
24
27
  teamId: string;
25
28
  agentId: string;
26
29
  limit?: number;
27
30
  workerId?: string;
28
- }) {
31
+ /** Disable event-driven chaining (used in tests to control execution order). */
32
+ noChain?: boolean;
33
+ }, chainDepth = 0) {
29
34
  const teamId = String(opts.teamId);
30
35
  const agentId = String(opts.agentId);
36
+ const noChain = !!opts.noChain;
31
37
  if (!teamId) throw new Error('--team-id is required');
32
38
  if (!agentId) throw new Error('--agent-id is required');
33
39
 
@@ -415,6 +421,44 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
415
421
  await fs.writeFile(artifactPath, JSON.stringify({ ok: true, tool: toolName, args: toolArgs, result }, null, 2) + '\n', 'utf8');
416
422
 
417
423
 
424
+ } else if (toolName === 'fs.write') {
425
+ const relPathRaw = String(toolArgs.path ?? '').trim();
426
+ const contentRaw = String(toolArgs.content ?? '');
427
+ if (!relPathRaw) throw new Error('fs.write requires args.path');
428
+
429
+ const vars = {
430
+ date: new Date().toISOString(),
431
+ 'run.id': runId,
432
+ 'run.timestamp': runId,
433
+ 'workflow.id': String(workflow.id ?? ''),
434
+ 'workflow.name': String(workflow.name ?? workflow.id ?? workflowFile),
435
+ };
436
+ // Also inject node outputs so templates like {{brand_review.output}} resolve
437
+ const { run: runSnap } = await loadRunFile(teamDir, runsDir, task.runId);
438
+ for (const nr of (runSnap.nodeResults ?? [])) {
439
+ const nid = String((nr as Record<string, unknown>).nodeId ?? '');
440
+ const nrOutPath = String((nr as Record<string, unknown>).nodeOutputPath ?? '');
441
+ if (nid && nrOutPath) {
442
+ try {
443
+ const outAbs = path.resolve(teamDir, nrOutPath);
444
+ vars[`${nid}.output`] = await fs.readFile(outAbs, 'utf8');
445
+ } catch { /* node output may not exist */ }
446
+ }
447
+ }
448
+ const relPath = templateReplace(relPathRaw, vars);
449
+ const content = templateReplace(contentRaw, vars);
450
+
451
+ const abs = path.resolve(teamDir, relPath);
452
+ if (!abs.startsWith(teamDir + path.sep) && abs !== teamDir) {
453
+ throw new Error('fs.write path must be within the team workspace');
454
+ }
455
+
456
+ await ensureDir(path.dirname(abs));
457
+ await fs.writeFile(abs, content, 'utf8');
458
+
459
+ const result = { writtenTo: path.relative(teamDir, abs), bytes: Buffer.byteLength(content, 'utf8') };
460
+ await fs.writeFile(artifactPath, JSON.stringify({ ok: true, tool: toolName, args: toolArgs, result }, null, 2) + '\n', 'utf8');
461
+
418
462
  } else if (toolName === 'marketing.post_all') {
419
463
  // Disabled by default: do not ship plugins that spawn local processes for posting.
420
464
  // Use an approval-gated workflow node that calls a dedicated posting tool/plugin instead.
@@ -540,6 +584,12 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
540
584
  events: [...cur.events, { ts: new Date().toISOString(), type: 'node.enqueued', nodeId: nextNode.id, agentId: approvalAgentId }],
541
585
  }));
542
586
 
587
+ // Event-driven chaining: immediately kick the next agent's worker
588
+ if (!noChain && approvalAgentId !== agentId && chainDepth < maxChainDepth) {
589
+ void runWorkflowWorkerTick(api, { teamId, agentId: approvalAgentId, limit: 1, workerId: `${workerId}:chain` }, chainDepth + 1)
590
+ .catch(() => { /* best-effort — cron workers are the safety net */ });
591
+ }
592
+
543
593
  results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'ok' });
544
594
  continue;
545
595
  }
@@ -562,6 +612,12 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
562
612
  events: [...cur.events, { ts: new Date().toISOString(), type: 'node.enqueued', nodeId: nextNode.id, agentId: nextAgentId }],
563
613
  }));
564
614
 
615
+ // Event-driven chaining: immediately kick the next agent's worker
616
+ if (!noChain && nextAgentId !== agentId && chainDepth < maxChainDepth) {
617
+ void runWorkflowWorkerTick(api, { teamId, agentId: nextAgentId, limit: 1, workerId: `${workerId}:chain` }, chainDepth + 1)
618
+ .catch(() => { /* best-effort — cron workers are the safety net */ });
619
+ }
620
+
565
621
  results.push({ taskId: task.id, runId: task.runId, nodeId: task.nodeId, status: 'ok' });
566
622
  } finally {
567
623
  if (lockHeld) {