@jiggai/recipes 0.3.6 → 0.3.8

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
@@ -26,6 +26,7 @@ import {
26
26
  } from "./src/handlers/install";
27
27
  import {
28
28
  handleAssign,
29
+ handleCleanupClosedAssignments,
29
30
  handleDispatch,
30
31
  handleHandoff,
31
32
  handleMoveTicket,
@@ -482,6 +483,19 @@ const recipesPlugin = {
482
483
  console.log(JSON.stringify({ ok: true, moved: { from: res.from, to: res.to } }, null, 2));
483
484
  });
484
485
 
486
+ cmd
487
+ .command("cleanup-closed-assignments")
488
+ .description("Archive assignment stubs for tickets already in work/done (prevents done work resurfacing)")
489
+ .requiredOption("--team-id <teamId>", "Team id")
490
+ .option("--ticket <ticketNums...>", "Optional ticket numbers to target (e.g. 0050 0064)")
491
+ .action(async (options: { teamId?: string; ticket?: string[] }) => {
492
+ if (!options.teamId) throw new Error("--team-id is required");
493
+ const res = await handleCleanupClosedAssignments(api, {
494
+ teamId: options.teamId,
495
+ ticketNums: options.ticket,
496
+ });
497
+ console.log(JSON.stringify(res, null, 2));
498
+ });
485
499
 
486
500
  cmd
487
501
  .command("assign")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -59,6 +59,21 @@ agents:
59
59
  deny: ["exec"]
60
60
 
61
61
  templates:
62
+ tools: |
63
+ # TOOLS.md
64
+
65
+ # Agent-local notes (paths, conventions, env quirks).
66
+
67
+ status: |
68
+ # STATUS.md
69
+
70
+ - (empty)
71
+
72
+ notes: |
73
+ # NOTES.md
74
+
75
+ - (empty)
76
+
62
77
  lead.soul: |
63
78
  # SOUL.md
64
79
 
@@ -0,0 +1,485 @@
1
+ ---
2
+ id: swarm-orchestrator
3
+ name: Swarm Orchestrator
4
+ version: 0.2.0
5
+ description: Scaffold an OpenClaw “orchestrator” workspace that spawns coding agents in tmux + git worktrees and monitors them via a lightweight task registry.
6
+ kind: agent
7
+ requiredSkills: []
8
+ cronJobs:
9
+ - id: swarm-monitor-loop
10
+ name: "Swarm monitor loop"
11
+ schedule: "*/10 * * * *"
12
+ timezone: "America/New_York"
13
+ message: "Reminder: swarm monitor loop — run .clawdbot/check-agents.sh to detect stuck/failed tmux agents, PR/CI state, and decide whether to notify or retry. Update .clawdbot/active-tasks.json as needed."
14
+ enabledByDefault: false
15
+ - id: swarm-cleanup-loop
16
+ name: "Swarm cleanup loop"
17
+ schedule: "17 4 * * *"
18
+ timezone: "America/New_York"
19
+ message: "Reminder: swarm cleanup loop — consider running .clawdbot/cleanup.sh to prune completed worktrees, closed PR branches, and dead tmux sessions (safe-by-default; no deletes unless explicitly enabled)."
20
+ enabledByDefault: false
21
+
22
+ templates:
23
+ soul: |
24
+ # SOUL.md
25
+
26
+ You are an orchestration agent (“swarm orchestrator”).
27
+
28
+ You do NOT primarily write code.
29
+
30
+ Your job is to:
31
+ - translate business context into sharp prompts + constraints
32
+ - spawn focused coding agents into isolated git worktrees + tmux sessions
33
+ - monitor them and steer when needed
34
+ - only notify a human when a PR is truly ready for review
35
+
36
+ Guardrails:
37
+ - Keep changes small and PR-shaped.
38
+ - Don’t delete worktrees/branches unless the user explicitly opts in.
39
+ - Always use the prompt template in `.clawdbot/PROMPT_TEMPLATE.md`.
40
+
41
+ agents: |
42
+ # AGENTS.md
43
+
44
+ ## Operating loop
45
+
46
+ 1) Read `.clawdbot/active-tasks.json`.
47
+ 2) For each task:
48
+ - confirm tmux session is alive
49
+ - confirm branch/worktree exists
50
+ - if configured, check PR/CI status
51
+ 3) Only ping the human when:
52
+ - a task is blocked and needs a decision
53
+ - a PR meets the default Definition of Done
54
+
55
+ ## Key files
56
+
57
+ - `.clawdbot/README.md` — setup + how to use
58
+ - `.clawdbot/CONVENTIONS.md` — default naming + how to change
59
+ - `.clawdbot/PROMPT_TEMPLATE.md` — required spawn prompt template
60
+ - `.clawdbot/TEMPLATE.md` — copy/paste helper for new tasks
61
+ - `.clawdbot/env.sh` — portable env configuration
62
+ - `.clawdbot/active-tasks.json` — task registry
63
+ - `.clawdbot/spawn.sh` — create worktree + start tmux session
64
+ - `.clawdbot/check-agents.sh` — monitor loop (token-efficient)
65
+ - `.clawdbot/cleanup.sh` — safe-by-default cleanup scaffold
66
+
67
+ readme: |
68
+ # Swarm Orchestrator
69
+
70
+ This scaffold gives you a lightweight “swarm” workflow:
71
+
72
+ - each coding agent runs in its own **git worktree** + **branch**
73
+ - each agent runs in its own **tmux session** (attach + steer mid-flight)
74
+ - a simple JSON registry (`active-tasks.json`) makes monitoring deterministic
75
+
76
+ The orchestrator’s job is to:
77
+ 1) translate a request into a tight prompt + constraints
78
+ 2) spawn 1+ coding agents in parallel
79
+ 3) monitor progress until a PR is truly ready for review
80
+
81
+ ---
82
+
83
+ ## 0) Prerequisites
84
+
85
+ Required:
86
+ - `git`
87
+ - `tmux`
88
+ - `jq`
89
+
90
+ Optional (recommended):
91
+ - GitHub CLI `gh` (for PR + CI status checks)
92
+ - run `gh auth login` to authenticate
93
+
94
+ Quick check:
95
+
96
+ ```bash
97
+ command -v git >/dev/null || echo "missing: git"
98
+ command -v tmux >/dev/null || echo "missing: tmux"
99
+ command -v jq >/dev/null || echo "missing: jq"
100
+
101
+ # optional:
102
+ command -v gh >/dev/null || echo "optional missing: gh (PR/CI monitoring)"
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 1) One-time setup
108
+
109
+ ### 1.1 Make scripts executable (manual)
110
+
111
+ This scaffold does **not** change file permissions automatically.
112
+
113
+ Run:
114
+
115
+ ```bash
116
+ chmod +x .clawdbot/*.sh
117
+ ```
118
+
119
+ ### 1.2 Configure environment
120
+
121
+ Edit: `.clawdbot/env.sh`
122
+
123
+ Set:
124
+ - `SWARM_REPO_DIR` — absolute path to the repo you want agents to work on
125
+ - `SWARM_WORKTREE_ROOT` — absolute path where worktrees will be created
126
+ - recommended: a dedicated folder (NOT inside your repo folder, and NOT inside the OpenClaw workspace)
127
+ - `SWARM_BASE_REF` — base ref to branch from (default: `origin/main`)
128
+
129
+ Optional:
130
+ - `SWARM_AGENT_RUNNER` — wrapper to start your chosen coding agent CLI (Codex / Claude Code / etc.)
131
+
132
+ ---
133
+
134
+ ## 2) Conventions (defaults)
135
+
136
+ Default conventions are in:
137
+ - `.clawdbot/CONVENTIONS.md`
138
+
139
+ If you want to customize naming or the Definition of Done, change it there.
140
+
141
+ ---
142
+
143
+ ## 3) Spawning agents
144
+
145
+ Basic spawn:
146
+
147
+ ```bash
148
+ ./.clawdbot/spawn.sh <branch-slug> <codex|claude> <tmux-session> [model] [reasoning]
149
+ ```
150
+
151
+ Example:
152
+
153
+ ```bash
154
+ ./.clawdbot/spawn.sh feat/0082-attempt-a codex swarm-0082-a gpt-5.3-codex high
155
+ ```
156
+
157
+ Attach:
158
+
159
+ ```bash
160
+ tmux attach -t swarm-0082-a
161
+ ```
162
+
163
+ Spawning more than one agent = run `spawn.sh` multiple times with different branch slugs + tmux session names.
164
+
165
+ ---
166
+
167
+ ## 4) Task registry
168
+
169
+ File: `.clawdbot/active-tasks.json`
170
+
171
+ Keep it accurate. Monitoring should read the registry, not guess.
172
+
173
+ ---
174
+
175
+ ## 5) Monitoring
176
+
177
+ ```bash
178
+ ./.clawdbot/check-agents.sh
179
+ ```
180
+
181
+ This is intentionally simple and deterministic. Extend it to include PR/CI checks if desired.
182
+
183
+ ---
184
+
185
+ ## 6) Default Definition of Done (notify-ready)
186
+
187
+ A PR is **ready for human review** when:
188
+ - PR exists
189
+ - branch is mergeable and up to date with base
190
+ - CI is green (lint/types/tests as appropriate)
191
+ - if the PR includes **UI changes**, include screenshots in the PR description
192
+ - a human still performs the final review + merge decision (default gate)
193
+
194
+ conventions: |
195
+ # Swarm Conventions (Defaults)
196
+
197
+ These defaults are designed to be portable across users and repos.
198
+
199
+ ## Naming
200
+
201
+ ### Branch naming
202
+
203
+ Recommended:
204
+ - `feat/<ticket>-<slug>`
205
+ - `fix/<ticket>-<slug>`
206
+
207
+ Examples:
208
+ - `feat/0082-swarm-orchestrator`
209
+ - `fix/0141-login-redirect`
210
+
211
+ If you do not use tickets:
212
+ - `feat/<slug>` / `fix/<slug>` is fine.
213
+
214
+ ### tmux session naming
215
+
216
+ Recommended:
217
+ - `swarm-<ticket>-<suffix>`
218
+
219
+ Examples:
220
+ - `swarm-0082-a`
221
+ - `swarm-0082-b`
222
+
223
+ ## Directory layout
224
+
225
+ - Orchestrator workspace: OpenClaw agent workspace (scaffold output)
226
+ - Worktrees root: set via `SWARM_WORKTREE_ROOT` (recommended: dedicated folder outside repo + outside OpenClaw workspace)
227
+
228
+ ## Definition of Done (default)
229
+
230
+ A PR is ready for human review when:
231
+ - PR exists
232
+ - mergeable + up-to-date
233
+ - CI green
234
+ - screenshots included *only if UI changes*
235
+ - human gate remains (default)
236
+
237
+ promptTemplate: |
238
+ # Swarm Coding-Agent Prompt Template
239
+
240
+ Copy this template when spawning a coding agent.
241
+
242
+ IMPORTANT:
243
+ - Do not freestyle prompts.
244
+ - Keep scope tight.
245
+ - Optimize for a small, reviewable PR.
246
+
247
+ ---
248
+
249
+ ## Ticket / Goal
250
+
251
+ - Ticket (optional): <NNNN>
252
+ - Goal: <one sentence>
253
+
254
+ ## Context
255
+
256
+ <why this matters / user story / product intent>
257
+
258
+ ## Requirements
259
+
260
+ - <requirement 1>
261
+ - <requirement 2>
262
+
263
+ ## Constraints (very important)
264
+
265
+ - Keep changes small and PR-shaped.
266
+ - Avoid refactors unless required to meet the requirements.
267
+ - If you need clarification, STOP and ask.
268
+ - If you touch UI, screenshots are required (otherwise omit screenshots).
269
+
270
+ ## Definition of Done (default)
271
+
272
+ - Code compiles/builds.
273
+ - CI is green (lint/types/tests as appropriate for this repo).
274
+ - PR is opened with a clear description.
275
+ - If UI changed: include before/after screenshots in the PR description.
276
+ - Do NOT merge. Leave it for human review.
277
+
278
+ ## File / Area hints (optional)
279
+
280
+ - Focus files:
281
+ - <path>
282
+ - <path>
283
+
284
+ ## Suggested plan
285
+
286
+ 1) <step>
287
+ 2) <step>
288
+
289
+ ## Deliverables
290
+
291
+ - A PR implementing the requirements.
292
+ - Notes in the PR description: what changed + how to verify.
293
+
294
+ taskTemplate: |
295
+ # Swarm Task Template
296
+
297
+ Use this as a starting point for a new entry in `active-tasks.json`.
298
+
299
+ - Decide the branch name using `CONVENTIONS.md`.
300
+ - Decide a unique tmux session name.
301
+
302
+ ```json
303
+ {
304
+ "id": "0082-attempt-a",
305
+ "ticket": "0082",
306
+ "description": "<short description>",
307
+ "branch": "feat/0082-attempt-a",
308
+ "worktree": "feat/0082-attempt-a",
309
+ "tmuxSession": "swarm-0082-a",
310
+ "agent": "codex",
311
+ "model": "gpt-5.3-codex",
312
+ "startedAt": 0,
313
+ "status": "queued",
314
+ "notifyOnComplete": true,
315
+ "prUrl": null,
316
+ "checks": {}
317
+ }
318
+ ```
319
+
320
+ env: |
321
+ # .clawdbot/env.sh
322
+ #
323
+ # Configure these for your environment.
324
+
325
+ # Absolute path to the repo you want to operate on.
326
+ export SWARM_REPO_DIR=""
327
+
328
+ # Absolute path where worktrees will be created.
329
+ # Recommended: a dedicated folder (NOT inside the repo folder, NOT inside the OpenClaw workspace).
330
+ export SWARM_WORKTREE_ROOT=""
331
+
332
+ # Default base ref to branch from.
333
+ export SWARM_BASE_REF="origin/main"
334
+
335
+ # Optional: path to your agent runner wrapper.
336
+ # This script/command should start Codex/Claude Code/etc inside the worktree.
337
+ export SWARM_AGENT_RUNNER=""
338
+
339
+ activeTasks: |
340
+ [
341
+ {
342
+ "id": "example-task",
343
+ "ticket": "",
344
+ "description": "Replace me",
345
+ "repo": "",
346
+ "worktree": "",
347
+ "branch": "",
348
+ "tmuxSession": "",
349
+ "agent": "codex",
350
+ "model": "",
351
+ "startedAt": 0,
352
+ "status": "queued",
353
+ "notifyOnComplete": true,
354
+ "prUrl": null,
355
+ "checks": {}
356
+ }
357
+ ]
358
+
359
+ spawn: |
360
+ #!/usr/bin/env bash
361
+ set -euo pipefail
362
+
363
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
364
+ # shellcheck disable=SC1091
365
+ source "$HERE/env.sh"
366
+
367
+ if [[ -z "${SWARM_REPO_DIR:-}" || -z "${SWARM_WORKTREE_ROOT:-}" || -z "${SWARM_BASE_REF:-}" ]]; then
368
+ echo "Missing env. Edit $HERE/env.sh (SWARM_REPO_DIR, SWARM_WORKTREE_ROOT, SWARM_BASE_REF)." >&2
369
+ exit 2
370
+ fi
371
+
372
+ BRANCH_SLUG="${1:-}"
373
+ AGENT_KIND="${2:-codex}" # codex|claude
374
+ TMUX_SESSION="${3:-}"
375
+ MODEL="${4:-}"
376
+ REASONING="${5:-medium}"
377
+
378
+ if [[ -z "$BRANCH_SLUG" || -z "$TMUX_SESSION" ]]; then
379
+ echo "Usage: $0 <branch-slug> <codex|claude> <tmux-session> [model] [reasoning]" >&2
380
+ exit 2
381
+ fi
382
+
383
+ WORKTREE_DIR="$SWARM_WORKTREE_ROOT/$BRANCH_SLUG"
384
+
385
+ echo "[swarm] Creating worktree: $WORKTREE_DIR"
386
+ mkdir -p "$SWARM_WORKTREE_ROOT"
387
+ cd "$SWARM_REPO_DIR"
388
+
389
+ git worktree add "$WORKTREE_DIR" -b "$BRANCH_SLUG" "$SWARM_BASE_REF"
390
+
391
+ echo "[swarm] Starting tmux session: $TMUX_SESSION"
392
+ if [[ -z "${SWARM_AGENT_RUNNER:-}" ]]; then
393
+ echo "SWARM_AGENT_RUNNER not set. Starting a shell in tmux; run your agent CLI manually." >&2
394
+ tmux new-session -d -s "$TMUX_SESSION" -c "$WORKTREE_DIR" "bash"
395
+ else
396
+ tmux new-session -d -s "$TMUX_SESSION" -c "$WORKTREE_DIR" \
397
+ "$SWARM_AGENT_RUNNER $AGENT_KIND ${MODEL:-} ${REASONING:-medium}"
398
+ fi
399
+
400
+ echo "[swarm] Done. Attach with: tmux attach -t $TMUX_SESSION"
401
+
402
+ checkAgents: |
403
+ #!/usr/bin/env bash
404
+ set -euo pipefail
405
+
406
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
407
+ REG="$HERE/active-tasks.json"
408
+
409
+ if [[ ! -f "$REG" ]]; then
410
+ echo "Missing $REG" >&2
411
+ exit 2
412
+ fi
413
+
414
+ echo "[swarm] Checking tmux sessions listed in active-tasks.json ..."
415
+
416
+ if ! command -v jq >/dev/null 2>&1; then
417
+ echo "jq is required" >&2
418
+ exit 2
419
+ fi
420
+
421
+ mapfile -t sessions < <(jq -r '.[].tmuxSession // empty' "$REG" | sort -u)
422
+ if [[ ${#sessions[@]} -eq 0 ]]; then
423
+ echo "[swarm] No tmux sessions found in registry."
424
+ exit 0
425
+ fi
426
+
427
+ for s in "${sessions[@]}"; do
428
+ if tmux has-session -t "$s" 2>/dev/null; then
429
+ echo "[ok] tmux session alive: $s"
430
+ else
431
+ echo "[dead] tmux session missing: $s"
432
+ fi
433
+ done
434
+
435
+ cleanup: |
436
+ #!/usr/bin/env bash
437
+ set -euo pipefail
438
+
439
+ echo "[swarm] Cleanup scaffold (safe-by-default)."
440
+ echo "- This script currently does NOT delete anything automatically."
441
+ echo "- Extend it to prune worktrees only after PRs are merged and branches are removed."
442
+
443
+ files:
444
+ - path: SOUL.md
445
+ template: soul
446
+ mode: createOnly
447
+ - path: AGENTS.md
448
+ template: agents
449
+ mode: createOnly
450
+ - path: .clawdbot/README.md
451
+ template: readme
452
+ mode: createOnly
453
+ - path: .clawdbot/CONVENTIONS.md
454
+ template: conventions
455
+ mode: createOnly
456
+ - path: .clawdbot/PROMPT_TEMPLATE.md
457
+ template: promptTemplate
458
+ mode: createOnly
459
+ - path: .clawdbot/TEMPLATE.md
460
+ template: taskTemplate
461
+ mode: createOnly
462
+ - path: .clawdbot/env.sh
463
+ template: env
464
+ mode: createOnly
465
+ - path: .clawdbot/active-tasks.json
466
+ template: activeTasks
467
+ mode: createOnly
468
+ - path: .clawdbot/spawn.sh
469
+ template: spawn
470
+ mode: createOnly
471
+ - path: .clawdbot/check-agents.sh
472
+ template: checkAgents
473
+ mode: createOnly
474
+ - path: .clawdbot/cleanup.sh
475
+ template: cleanup
476
+ mode: createOnly
477
+
478
+ tools:
479
+ profile: "coding"
480
+ allow: ["group:fs", "group:web", "group:runtime", "group:automation", "cron", "message"]
481
+ deny: []
482
+ ---
483
+ # Swarm Orchestrator
484
+
485
+ This is a workflow scaffold recipe. It creates a portable, file-first setup for running multiple coding agents in parallel using git worktrees + tmux.
@@ -34,17 +34,56 @@ export async function scaffoldAgentFromRecipe(
34
34
  ) {
35
35
  await ensureDir(opts.filesRootDir);
36
36
 
37
- const templates = recipe.templates ?? {};
38
- const files = recipe.files ?? [];
37
+ type RecipeFileMode = "createOnly" | "overwrite";
38
+ type RecipeFileSpec = {
39
+ path: string;
40
+ template: string;
41
+ mode?: RecipeFileMode;
42
+ };
43
+
44
+ function normalizeTemplates(input: unknown): Record<string, string> {
45
+ if (!input) return {};
46
+ if (typeof input !== "object") throw new Error("recipe.templates must be an object");
47
+ const out: Record<string, string> = {};
48
+ for (const [k, v] of Object.entries(input as Record<string, unknown>)) {
49
+ if (typeof v === "string") out[k] = v;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function normalizeFiles(input: unknown): RecipeFileSpec[] {
55
+ if (!input) return [];
56
+ if (!Array.isArray(input)) throw new Error("recipe.files must be an array");
57
+
58
+ return input.map((raw, idx) => {
59
+ if (!raw || typeof raw !== "object") throw new Error(`recipe.files[${idx}] must be an object`);
60
+ const o = raw as Record<string, unknown>;
61
+ const filePath = String(o.path ?? "").trim();
62
+ const template = String(o.template ?? "").trim();
63
+ const modeRaw = o.mode != null ? String(o.mode).trim() : "";
64
+
65
+ if (!filePath) throw new Error(`recipe.files[${idx}].path is required`);
66
+ if (!template) throw new Error(`recipe.files[${idx}].template is required`);
67
+
68
+ const mode: RecipeFileMode | undefined =
69
+ modeRaw === "createOnly" || modeRaw === "overwrite" ? (modeRaw as RecipeFileMode) : undefined;
70
+ if (modeRaw && !mode) throw new Error(`recipe.files[${idx}].mode must be createOnly|overwrite`);
71
+
72
+ return { path: filePath, template, mode };
73
+ });
74
+ }
75
+
76
+ const templates = normalizeTemplates(recipe.templates);
77
+ const files = normalizeFiles(recipe.files);
39
78
  const vars = opts.vars ?? {};
40
79
 
41
80
  const fileResults: Array<{ path: string; wrote: boolean; reason: string }> = [];
42
81
  for (const f of files) {
43
82
  const raw = templates[f.template];
44
- if (typeof raw !== "string") throw new Error(`Missing template: ${f.template}`);
83
+ if (typeof raw !== "string") throw new Error(`Missing template: ${String(f.template)}`);
45
84
  const rendered = renderTemplate(raw, vars);
46
85
  const target = path.join(opts.filesRootDir, f.path);
47
- const mode = opts.update ? (f.mode ?? "overwrite") : (f.mode ?? "createOnly");
86
+ const mode: RecipeFileMode = opts.update ? (f.mode ?? "overwrite") : (f.mode ?? "createOnly");
48
87
  const r = await writeFileSafely(target, rendered, mode);
49
88
  fileResults.push({ path: target, wrote: r.wrote, reason: r.reason });
50
89
  }
@@ -325,3 +325,59 @@ export async function handleDispatch(
325
325
  }
326
326
  return { ok: true as const, wrote: plan.files.map((f) => f.path), nudgeQueued };
327
327
  }
328
+
329
+ /**
330
+ * Cleanup assignment stubs for tickets that are already closed (in work/done).
331
+ *
332
+ * Why: some automation/board views treat assignment stubs as active work signals.
333
+ * If a ticket is manually moved to done (outside `openclaw recipes move-ticket`),
334
+ * its `work/assignments/<num>-assigned-*.md` stubs may linger and resurface the ticket.
335
+ *
336
+ * This command archives any matching assignment stubs into `work/assignments/archive/`.
337
+ */
338
+ export async function handleCleanupClosedAssignments(
339
+ api: OpenClawPluginApi,
340
+ options: { teamId: string; ticketNums?: string[] }
341
+ ): Promise<{ ok: true; teamId: string; archived: Array<{ from: string; to: string }> }> {
342
+ const teamId = String(options.teamId);
343
+ const { teamDir } = await resolveTeamContext(api, teamId);
344
+
345
+ const assignmentsDir = ticketStageDir(teamDir, "assignments");
346
+ const archiveDir = path.join(assignmentsDir, "archive");
347
+ const doneDir = ticketStageDir(teamDir, "done");
348
+
349
+ const archived: Array<{ from: string; to: string }> = [];
350
+ if (!(await fileExists(assignmentsDir))) return { ok: true, teamId, archived };
351
+ await ensureDir(archiveDir);
352
+
353
+ const ticketNumsFilter = Array.isArray(options.ticketNums) && options.ticketNums.length
354
+ ? new Set(options.ticketNums.map((n) => String(n).padStart(4, "0")))
355
+ : null;
356
+
357
+ const doneFiles = (await fileExists(doneDir)) ? await fs.readdir(doneDir) : [];
358
+ const doneNums = new Set(
359
+ doneFiles
360
+ .map((f) => f.match(/^([0-9]{4})-/)?.[1])
361
+ .filter((x): x is string => !!x)
362
+ );
363
+
364
+ const files = (await fs.readdir(assignmentsDir)).filter((f) => f.endsWith(".md"));
365
+ for (const f of files) {
366
+ if (f === "archive") continue;
367
+ if (f.startsWith("archive" + path.sep)) continue;
368
+ const m = f.match(/^([0-9]{4})-assigned-.*\.md$/);
369
+ if (!m) continue;
370
+ const num = m[1];
371
+ if (ticketNumsFilter && !ticketNumsFilter.has(num)) continue;
372
+
373
+ // If the ticket number is present in done/, this assignment is considered closed.
374
+ if (!doneNums.has(num)) continue;
375
+
376
+ const from = path.join(assignmentsDir, f);
377
+ const to = path.join(archiveDir, f);
378
+ await fs.rename(from, to);
379
+ archived.push({ from, to });
380
+ }
381
+
382
+ return { ok: true, teamId, archived };
383
+ }