@clawcipes/recipes 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcipes/recipes",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Clawcipes recipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "index.ts",
17
+ "src/",
17
18
  "openclaw.plugin.json",
18
19
  "recipes/",
19
20
  "docs/",
@@ -21,8 +22,10 @@
21
22
  "README.md"
22
23
  ],
23
24
  "scripts": {
24
- "test": "npm run smoke",
25
- "smoke": "node scripts/scaffold-smoke.mjs"
25
+ "test": "vitest run",
26
+ "test:smoke": "node scripts/scaffold-smoke.mjs",
27
+ "smoke": "node scripts/scaffold-smoke.mjs",
28
+ "scaffold:smoke": "node scripts/scaffold-smoke.mjs"
26
29
  },
27
30
  "keywords": [
28
31
  "openclaw",
@@ -36,5 +39,8 @@
36
39
  "@sinclair/typebox": "^0.34.48",
37
40
  "json5": "^2.2.3",
38
41
  "yaml": "^2.8.2"
42
+ },
43
+ "devDependencies": {
44
+ "vitest": "^3.2.4"
39
45
  }
40
46
  }
@@ -4,6 +4,20 @@ name: Customer Support Team
4
4
  version: 0.1.0
5
5
  description: A support workflow team (triage, resolver, kb-writer) that turns cases into replies and knowledge base articles.
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: customer-support-team
@@ -55,17 +69,19 @@ templates:
55
69
  - inbox/ — incoming cases / requests
56
70
  - work/backlog/ — tickets (filename ordered: 0001-...)
57
71
  - work/in-progress/ — active tickets
72
+ - work/testing/ — verification / customer-ready review
58
73
  - work/done/ — completed tickets + DONE notes
59
74
  - work/cases/ — case records (one per customer issue)
60
75
  - work/replies/ — draft replies
61
76
  - work/kb/ — KB drafts and macros
62
77
  - outbox/ — finalized replies + KB articles
63
78
 
64
- ## Dispatch loop
79
+ ## Dispatch loop (mapped to canonical lanes)
65
80
  1) Create a case file in work/cases/
66
- 2) Create a ticket in work/backlog/
67
- 3) Assign triage resolver kb-writer as needed
68
- 4) Finalize into outbox/
81
+ 2) Create a ticket in work/backlog/ (triage queue)
82
+ 3) Move to work/in-progress/ for resolution + drafting reply
83
+ 4) Move to work/testing/ for verification (customer-ready review)
84
+ 5) Move to work/done/ and finalize into outbox/
69
85
 
70
86
  ## Quality bar
71
87
  - Ask for missing info early.
@@ -117,6 +133,12 @@ templates:
117
133
  - step-by-step
118
134
  - Include links to docs when relevant.
119
135
 
136
+ ## Verification
137
+ Before the ticket is moved to done/outbox:
138
+ - Move the ticket to work/testing/ for verification.
139
+ - Record verification using notes/QA_CHECKLIST.md.
140
+ - Preferred: create work/testing/<ticket>.testing-verified.md.
141
+
120
142
  kb-writer.soul: |
121
143
  # SOUL.md
122
144
 
@@ -4,6 +4,20 @@ name: Development Team
4
4
  version: 0.2.0
5
5
  description: A small engineering team with a shared workspace (lead, dev, devops, test) using file-first tickets.
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: development-team
@@ -4,6 +4,20 @@ name: Product Team
4
4
  version: 0.1.0
5
5
  description: A product delivery team (pm, designer, engineer, qa) that turns ideas into shipped features.
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: product-team
@@ -32,8 +46,8 @@ agents:
32
46
  profile: "coding"
33
47
  allow: ["group:fs", "group:web", "group:runtime"]
34
48
  deny: []
35
- - role: qa
36
- name: QA / Test Planner
49
+ - role: test
50
+ name: QA / Tester
37
51
  tools:
38
52
  profile: "coding"
39
53
  allow: ["group:fs", "group:web"]
@@ -134,24 +148,27 @@ templates:
134
148
  - Implement
135
149
  - Write a DONE note with how to test
136
150
 
137
- qa.soul: |
151
+ test.soul: |
138
152
  # SOUL.md
139
153
 
140
- You are QA / Test Planner on {{teamId}}.
154
+ You are QA / Testing on {{teamId}}.
141
155
 
142
- You create pragmatic test plans and catch edge cases.
156
+ You verify acceptance criteria, catch edge cases, and document verification results.
143
157
 
144
- qa.agents: |
158
+ test.agents: |
145
159
  # AGENTS.md
146
160
 
147
161
  Team directory: {{teamDir}}
148
162
 
163
+ How you work:
164
+ 1) Look in work/testing/ for tickets assigned to you (Owner: test).
165
+ 2) Follow the ticket's "How to test" steps and validate acceptance criteria.
166
+ 3) Record verification using notes/QA_CHECKLIST.md (preferred: a sibling *.testing-verified.md note).
167
+ 4) If PASS: move ticket to work/done/.
168
+ 5) If FAIL: move ticket back to work/in-progress/ with clear repro steps.
169
+
149
170
  Output conventions:
150
- - Test plans go in work/test-plans/
151
- - Include:
152
- - happy path
153
- - edge cases
154
- - regression checklist
171
+ - Test plans (optional) go in work/test-plans/
155
172
 
156
173
  lead.tools: |
157
174
  # TOOLS.md
@@ -213,17 +230,17 @@ templates:
213
230
 
214
231
  - (empty)
215
232
 
216
- qa.tools: |
233
+ test.tools: |
217
234
  # TOOLS.md
218
235
 
219
- # Agent-local notes for qa (paths, conventions, env quirks).
236
+ # Agent-local notes for test (paths, conventions, env quirks).
220
237
 
221
- qa.status: |
238
+ test.status: |
222
239
  # STATUS.md
223
240
 
224
241
  - (empty)
225
242
 
226
- qa.notes: |
243
+ test.notes: |
227
244
  # NOTES.md
228
245
 
229
246
  - (empty)
@@ -4,6 +4,20 @@ name: Research Team
4
4
  version: 0.1.0
5
5
  description: A research team (lead, researcher, fact-checker, summarizer) that produces sourced briefs and notes.
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: research-team
@@ -55,6 +69,7 @@ templates:
55
69
  - inbox/ — intake requests
56
70
  - work/backlog/ — tickets (filename ordered: 0001-...)
57
71
  - work/in-progress/ — active tickets
72
+ - work/testing/ — verification / fact-check / review
58
73
  - work/done/ — completed tickets + DONE notes
59
74
  - work/sources/ — source links + captured quotes
60
75
  - work/notes/ — working notes
@@ -70,7 +85,8 @@ templates:
70
85
  1) Read new items in inbox/
71
86
  2) Create a normalized ticket in work/backlog/
72
87
  3) Assign an owner (researcher/fact-checker/summarizer)
73
- 4) When done, consolidate into outbox/
88
+ 4) When ready for verification, move the ticket to work/testing/
89
+ 5) After verification, move to work/done/ and consolidate into outbox/
74
90
 
75
91
  researcher.soul: |
76
92
  # SOUL.md
@@ -117,6 +133,10 @@ templates:
117
133
 
118
134
  - Write results in work/notes/fact-check-<ticket>.md.
119
135
 
136
+ ## QA verification
137
+ - When verification is complete, record results using notes/QA_CHECKLIST.md.
138
+ - Preferred: create work/testing/<ticket>.testing-verified.md.
139
+
120
140
  summarizer.soul: |
121
141
  # SOUL.md
122
142
 
@@ -4,6 +4,20 @@ name: Social Media Team
4
4
  version: 0.1.0
5
5
  description: A small social media team with a shared workspace (lead, research, writer, editor).
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: social-team
@@ -42,11 +56,16 @@ templates:
42
56
  Team: {{teamId}}
43
57
  Team directory: {{teamDir}}
44
58
 
45
- Workflow:
46
- - Intake: check `inbox/`
47
- - Assign: write tasks into `work/assignments/`
48
- - Review: consolidate drafts from `work/`
49
- - Deliver: finalize into `outbox/`
59
+ Workflow (mapped to canonical lanes):
60
+ - backlog in-progress → testing → done
61
+ - Intake: check `inbox/` and write tickets into work/backlog/
62
+ - Drafting: use work/in-progress/ for active drafting
63
+ - Approval/review: use work/testing/ for review + final checks
64
+ - Done: move to work/done/ and publish/schedule into outbox/
65
+
66
+ QA verification:
67
+ - Use notes/QA_CHECKLIST.md
68
+ - Preferred record: work/testing/<ticket>.testing-verified.md
50
69
 
51
70
  research.soul: |
52
71
  # SOUL.md
@@ -93,6 +112,71 @@ templates:
93
112
  - Edited drafts go in `work/edited/`.
94
113
  - Provide a short changelog at the top.
95
114
 
115
+ ## QA verification (approval)
116
+ Before moving a deliverable to done/scheduled:
117
+ - Record verification using notes/QA_CHECKLIST.md.
118
+ - Preferred: create work/testing/<ticket>.testing-verified.md.
119
+
120
+ lead.tools: |
121
+ # TOOLS.md
122
+
123
+ (empty)
124
+
125
+ lead.status: |
126
+ # STATUS.md
127
+
128
+ - (empty)
129
+
130
+ lead.notes: |
131
+ # NOTES.md
132
+
133
+ - (empty)
134
+
135
+ research.tools: |
136
+ # TOOLS.md
137
+
138
+ (empty)
139
+
140
+ research.status: |
141
+ # STATUS.md
142
+
143
+ - (empty)
144
+
145
+ research.notes: |
146
+ # NOTES.md
147
+
148
+ - (empty)
149
+
150
+ writer.tools: |
151
+ # TOOLS.md
152
+
153
+ (empty)
154
+
155
+ writer.status: |
156
+ # STATUS.md
157
+
158
+ - (empty)
159
+
160
+ writer.notes: |
161
+ # NOTES.md
162
+
163
+ - (empty)
164
+
165
+ editor.tools: |
166
+ # TOOLS.md
167
+
168
+ (empty)
169
+
170
+ editor.status: |
171
+ # STATUS.md
172
+
173
+ - (empty)
174
+
175
+ editor.notes: |
176
+ # NOTES.md
177
+
178
+ - (empty)
179
+
96
180
 
97
181
  files:
98
182
  - path: SOUL.md
@@ -4,6 +4,20 @@ name: Writing Team
4
4
  version: 0.1.0
5
5
  description: A writing pipeline (lead, outliner, writer, editor) that produces drafts and polished deliverables.
6
6
  kind: team
7
+ cronJobs:
8
+ - id: lead-triage-loop
9
+ name: "Lead triage loop"
10
+ schedule: "*/30 7-23 * * 1-5"
11
+ timezone: "America/New_York"
12
+ message: "Automated lead triage loop: triage inbox/tickets, assign work, and update notes/status.md."
13
+ enabledByDefault: false
14
+ - id: execution-loop
15
+ name: "Execution loop"
16
+ schedule: "*/30 7-23 * * 1-5"
17
+ timezone: "America/New_York"
18
+ message: "Automated execution loop: make progress on in-progress tickets, keep changes small/safe, and update notes/status.md."
19
+ enabledByDefault: false
20
+ # pr-watcher omitted (enable only when a real PR integration exists)
7
21
  requiredSkills: []
8
22
  team:
9
23
  teamId: writing-team
@@ -54,6 +68,7 @@ templates:
54
68
  - inbox/ — requests
55
69
  - work/backlog/ — tickets (0001-...)
56
70
  - work/in-progress/ — active tickets
71
+ - work/testing/ — review/edit/QA (verification before publishing)
57
72
  - work/done/ — completed tickets + DONE notes
58
73
  - work/briefs/ — writing briefs
59
74
  - work/outlines/ — outlines
@@ -64,8 +79,9 @@ templates:
64
79
  ## Dispatch loop
65
80
  1) Intake in inbox/
66
81
  2) Brief in work/briefs/
67
- 3) Assign outline → draft → edit
68
- 4) Finalize to outbox/
82
+ 3) Assign outline → draft
83
+ 4) Move to work/testing/ for edit/review
84
+ 5) After verification (see notes/QA_CHECKLIST.md), move to work/done/ and finalize to outbox/
69
85
 
70
86
  outliner.soul: |
71
87
  # SOUL.md
@@ -120,6 +136,11 @@ templates:
120
136
  - Provide a short changelog at the top.
121
137
  - Flag any factual claims that need citations.
122
138
 
139
+ ## QA verification
140
+ Before a deliverable is marked done/published:
141
+ - Record verification using notes/QA_CHECKLIST.md.
142
+ - Preferred: create work/testing/<ticket>.testing-verified.md.
143
+
123
144
  lead.tools: |
124
145
  # TOOLS.md
125
146
 
@@ -0,0 +1,59 @@
1
+ // Minimal extracted binding helper so we can test precedence without running the CLI.
2
+
3
+ import crypto from 'node:crypto';
4
+
5
+ export type BindingMatch = {
6
+ channel?: string;
7
+ peer?: string;
8
+ [k: string]: any;
9
+ };
10
+
11
+ export type BindingSnippet = {
12
+ agentId: string;
13
+ match: BindingMatch;
14
+ to: any;
15
+ enabled?: boolean;
16
+ };
17
+
18
+ function stableStringify(obj: any) {
19
+ const seen = new WeakSet();
20
+ const walk = (x: any): any => {
21
+ if (x && typeof x === 'object') {
22
+ if (seen.has(x)) return '[Circular]';
23
+ seen.add(x);
24
+ if (Array.isArray(x)) return x.map(walk);
25
+ const keys = Object.keys(x).sort();
26
+ const out: any = {};
27
+ for (const k of keys) out[k] = walk(x[k]);
28
+ return out;
29
+ }
30
+ return x;
31
+ };
32
+ return JSON.stringify(walk(obj));
33
+ }
34
+
35
+ export function upsertBindingInConfig(cfgObj: any, binding: BindingSnippet) {
36
+ if (!Array.isArray(cfgObj.bindings)) cfgObj.bindings = [];
37
+ const list: any[] = cfgObj.bindings;
38
+
39
+ const sigPayload = stableStringify({ agentId: binding.agentId, match: binding.match });
40
+ const sig = crypto.createHash('sha256').update(sigPayload).digest('hex');
41
+
42
+ const idx = list.findIndex((b: any) => {
43
+ const payload = stableStringify({ agentId: b.agentId, match: b.match });
44
+ const bsig = crypto.createHash('sha256').update(payload).digest('hex');
45
+ return bsig === sig;
46
+ });
47
+
48
+ if (idx >= 0) {
49
+ // Update in place (preserve ordering)
50
+ list[idx] = { ...list[idx], ...binding };
51
+ return { changed: false as const, note: 'already-present' as const };
52
+ }
53
+
54
+ // Most-specific-first: if a peer match is specified, insert at front so it wins.
55
+ if (binding.match?.peer) list.unshift(binding);
56
+ else list.push(binding);
57
+
58
+ return { changed: true as const, note: 'added' as const };
59
+ }
@@ -0,0 +1,173 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export type CleanupDecision =
5
+ | { kind: 'candidate'; teamId: string; dirName: string; absPath: string }
6
+ | { kind: 'skip'; teamId?: string; dirName: string; absPath: string; reason: string };
7
+
8
+ export type CleanupPlan = {
9
+ rootDir: string;
10
+ prefixes: string[];
11
+ protectedTeamIds: string[];
12
+ decisions: CleanupDecision[];
13
+ };
14
+
15
+ export const DEFAULT_ALLOWED_PREFIXES = ['smoke-', 'qa-', 'tmp-', 'test-'] as const;
16
+ export const DEFAULT_PROTECTED_TEAM_IDS = ['development-team'] as const;
17
+
18
+ async function isDir(p: string) {
19
+ try {
20
+ const st = await fs.stat(p);
21
+ return st.isDirectory();
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /** Refuse to operate on symlinks (for safety). */
28
+ async function isSymlink(p: string) {
29
+ try {
30
+ const st = await fs.lstat(p);
31
+ return st.isSymbolicLink();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ export function parseTeamIdFromWorkspaceDirName(dirName: string) {
38
+ if (!dirName.startsWith('workspace-')) return null;
39
+ const teamId = dirName.slice('workspace-'.length);
40
+ return teamId || null;
41
+ }
42
+
43
+ export function isEligibleTeamId(opts: { teamId: string; prefixes: string[]; protectedTeamIds: string[] }) {
44
+ const { teamId, prefixes, protectedTeamIds } = opts;
45
+
46
+ if (!teamId.endsWith('-team')) return { ok: false, reason: 'teamId does not end with -team' } as const;
47
+
48
+ if (protectedTeamIds.includes(teamId.replace(/-team$/, ''))) {
49
+ // Back-compat: protect by base id too if someone passes development-team-team etc.
50
+ return { ok: false, reason: 'protected teamId' } as const;
51
+ }
52
+ if (protectedTeamIds.includes(teamId)) return { ok: false, reason: 'protected teamId' } as const;
53
+
54
+ const okPrefix = prefixes.some((p) => teamId.startsWith(p));
55
+ if (!okPrefix) return { ok: false, reason: `teamId does not start with an allowed prefix (${prefixes.join(', ')})` } as const;
56
+
57
+ return { ok: true } as const;
58
+ }
59
+
60
+ /**
61
+ * Build a safe cleanup plan for workspace-<teamId> directories under rootDir.
62
+ *
63
+ * Safety rails:
64
+ * - only directories named workspace-<teamId>
65
+ * - teamId must end with -team and start with an allowed prefix
66
+ * - protected teamIds are always skipped
67
+ * - refuses symlinks
68
+ * - resolved path must remain within rootDir
69
+ */
70
+ export async function planWorkspaceCleanup(opts: {
71
+ rootDir: string;
72
+ prefixes?: string[];
73
+ protectedTeamIds?: string[];
74
+ }) {
75
+ const rootDir = path.resolve(opts.rootDir);
76
+ const prefixes = (opts.prefixes?.length ? opts.prefixes : [...DEFAULT_ALLOWED_PREFIXES]) as string[];
77
+ const protectedTeamIds = (opts.protectedTeamIds?.length ? opts.protectedTeamIds : [...DEFAULT_PROTECTED_TEAM_IDS]) as string[];
78
+
79
+ const decisions: CleanupDecision[] = [];
80
+
81
+ let entries: string[] = [];
82
+ try {
83
+ entries = await fs.readdir(rootDir);
84
+ } catch (e: any) {
85
+ return {
86
+ rootDir,
87
+ prefixes,
88
+ protectedTeamIds,
89
+ decisions: [
90
+ {
91
+ kind: 'skip',
92
+ dirName: rootDir,
93
+ absPath: rootDir,
94
+ reason: `failed to read rootDir: ${e?.message ? String(e.message) : String(e)}`,
95
+ },
96
+ ],
97
+ } satisfies CleanupPlan;
98
+ }
99
+
100
+ for (const dirName of entries) {
101
+ if (!dirName.startsWith('workspace-')) continue;
102
+
103
+ const absPath = path.join(rootDir, dirName);
104
+
105
+ if (!(await isDir(absPath))) continue;
106
+
107
+ const teamId = parseTeamIdFromWorkspaceDirName(dirName);
108
+ if (!teamId) {
109
+ decisions.push({ kind: 'skip', dirName, absPath, reason: 'could not parse teamId' });
110
+ continue;
111
+ }
112
+
113
+ if (await isSymlink(absPath)) {
114
+ decisions.push({ kind: 'skip', teamId, dirName, absPath, reason: 'refusing to operate on symlink' });
115
+ continue;
116
+ }
117
+
118
+ const real = await fs.realpath(absPath);
119
+ const rootReal = await fs.realpath(rootDir);
120
+ if (!real.startsWith(rootReal + path.sep) && real !== rootReal) {
121
+ decisions.push({ kind: 'skip', teamId, dirName, absPath, reason: 'resolved path escapes rootDir' });
122
+ continue;
123
+ }
124
+
125
+ const elig = isEligibleTeamId({ teamId, prefixes, protectedTeamIds });
126
+ if (!elig.ok) {
127
+ decisions.push({ kind: 'skip', teamId, dirName, absPath, reason: elig.reason });
128
+ continue;
129
+ }
130
+
131
+ decisions.push({ kind: 'candidate', teamId, dirName, absPath });
132
+ }
133
+
134
+ return { rootDir, prefixes, protectedTeamIds, decisions } satisfies CleanupPlan;
135
+ }
136
+
137
+ export async function executeWorkspaceCleanup(plan: CleanupPlan, opts: { yes: boolean }) {
138
+ const candidates = plan.decisions.filter((d): d is Extract<CleanupDecision, { kind: 'candidate' }> => d.kind === 'candidate');
139
+ const skipped = plan.decisions.filter((d): d is Extract<CleanupDecision, { kind: 'skip' }> => d.kind === 'skip');
140
+
141
+ if (!opts.yes) {
142
+ return {
143
+ ok: true,
144
+ dryRun: true,
145
+ rootDir: plan.rootDir,
146
+ candidates,
147
+ skipped,
148
+ deleted: [] as string[],
149
+ };
150
+ }
151
+
152
+ const deleted: string[] = [];
153
+ const deleteErrors: Array<{ path: string; error: string }> = [];
154
+
155
+ for (const c of candidates) {
156
+ try {
157
+ await fs.rm(c.absPath, { recursive: true, force: true });
158
+ deleted.push(c.absPath);
159
+ } catch (e: any) {
160
+ deleteErrors.push({ path: c.absPath, error: e?.message ? String(e.message) : String(e) });
161
+ }
162
+ }
163
+
164
+ return {
165
+ ok: deleteErrors.length === 0,
166
+ dryRun: false,
167
+ rootDir: plan.rootDir,
168
+ candidates,
169
+ skipped,
170
+ deleted,
171
+ deleteErrors,
172
+ };
173
+ }
@@ -0,0 +1,5 @@
1
+ export * from './scaffold-templates';
2
+ export * from './bindings';
3
+ export * from './ticket-workflow';
4
+ export * from './recipe-frontmatter';
5
+ export * from './shared-context';