@jiggai/recipes 0.3.0 → 0.3.2
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/LICENSE +192 -0
- package/README.md +21 -6
- package/docs/releasing.md +42 -0
- package/docs/verify-built-in-team-recipes.md +9 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/recipes/default/business-team.md +1 -1
- package/recipes/default/clinic-team.md +20 -0
- package/recipes/default/construction-team.md +20 -0
- package/recipes/default/crypto-trader-team.md +20 -0
- package/recipes/default/customer-support-team.md +1 -1
- package/recipes/default/developer.md +0 -1
- package/recipes/default/development-team.md +10 -1
- package/recipes/default/editor.md +1 -1
- package/recipes/default/financial-planner-team.md +21 -0
- package/recipes/default/law-firm-team.md +22 -0
- package/recipes/default/marketing-team.md +22 -1
- package/recipes/default/product-team.md +1 -1
- package/recipes/default/research-team.md +1 -1
- package/recipes/default/researcher.md +1 -1
- package/recipes/default/stock-trader-team.md +279 -238
- package/recipes/default/writing-team.md +1 -1
- package/src/handlers/team.ts +46 -12
- package/src/lib/agent-config.ts +11 -1
- package/src/lib/recipe-frontmatter.ts +7 -0
- package/src/lib/remove-team.ts +17 -4
- package/src/lib/workspace.ts +15 -2
package/src/handlers/team.ts
CHANGED
|
@@ -39,14 +39,16 @@ async function ensureTeamDirectoryStructure(
|
|
|
39
39
|
]);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function writeTeamBootstrapFiles(
|
|
43
|
-
teamId: string
|
|
44
|
-
teamDir: string
|
|
45
|
-
sharedContextDir: string
|
|
46
|
-
notesDir: string
|
|
47
|
-
goalsDir: string
|
|
48
|
-
overwrite: boolean
|
|
49
|
-
|
|
42
|
+
async function writeTeamBootstrapFiles(opts: {
|
|
43
|
+
teamId: string;
|
|
44
|
+
teamDir: string;
|
|
45
|
+
sharedContextDir: string;
|
|
46
|
+
notesDir: string;
|
|
47
|
+
goalsDir: string;
|
|
48
|
+
overwrite: boolean;
|
|
49
|
+
qaChecklist?: boolean;
|
|
50
|
+
}) {
|
|
51
|
+
const { teamId, teamDir, sharedContextDir, notesDir, goalsDir, overwrite, qaChecklist } = opts;
|
|
50
52
|
const mode = overwrite ? "overwrite" : "createOnly";
|
|
51
53
|
await ensureDir(goalsDir);
|
|
52
54
|
await writeFileSafely(
|
|
@@ -66,6 +68,14 @@ async function writeTeamBootstrapFiles(
|
|
|
66
68
|
`# Goals folder — ${teamId}\n\nCreate one markdown file per goal in this directory.\n\nRecommended file naming:\n- short, kebab-case, no leading numbers (e.g. \`reduce-support-backlog.md\`)\n\nLink goals from:\n- notes/GOALS.md\n`,
|
|
67
69
|
mode
|
|
68
70
|
);
|
|
71
|
+
|
|
72
|
+
if (qaChecklist) {
|
|
73
|
+
await writeFileSafely(
|
|
74
|
+
path.join(notesDir, "QA_CHECKLIST.md"),
|
|
75
|
+
`# QA Checklist — ${teamId}\n\nUse this when verifying a ticket before moving it from work/testing/ → work/done/.\n\n## Checklist\n- [ ] Repro steps verified\n- [ ] Acceptance criteria met\n- [ ] No regressions in adjacent flows\n- [ ] Notes/screenshots attached (if relevant)\n\n## Verified by\n- QA: (name)\n- Date: (YYYY-MM-DD)\n\n## Links\n- Ticket: (path or URL)\n- PR/Commit: (optional)\n`,
|
|
76
|
+
mode,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
69
79
|
const ticketsMd = `# Tickets — ${teamId}\n\n## Naming\n- Backlog tickets live in work/backlog/\n- In-progress tickets live in work/in-progress/\n- Testing tickets live in work/testing/\n- Done tickets live in work/done/\n- Filename ordering is the queue: 0001-..., 0002-...\n\n## Stages\n- backlog → in-progress → testing → done\n\n## QA handoff\n- When work is ready for QA: move the ticket to \`work/testing/\` and assign to test.\n\n## Required fields\nEach ticket should include:\n- Title\n- Context\n- Requirements\n- Acceptance criteria\n- Owner (dev/devops/lead/test)\n- Status (queued/in-progress/testing/done)\n\n## Example\n\n\`\`\`md\n# 0001-example-ticket\n\nOwner: dev\nStatus: queued\n\n## Context\n...\n\n## Requirements\n- ...\n\n## Acceptance criteria\n- ...\n\`\`\`\n`;
|
|
70
80
|
await writeFileSafely(path.join(teamDir, "TICKETS.md"), ticketsMd, mode);
|
|
71
81
|
}
|
|
@@ -175,7 +185,6 @@ export async function handleScaffoldTeam(
|
|
|
175
185
|
await ensureDir(recipesDir);
|
|
176
186
|
const overwriteRecipe = !!options.overwriteRecipe;
|
|
177
187
|
const autoIncrement = !!options.autoIncrement;
|
|
178
|
-
|
|
179
188
|
const explicitRecipeId = typeof options.recipeIdExplicit === "string" ? String(options.recipeIdExplicit).trim() : "";
|
|
180
189
|
const baseRecipeId = explicitRecipeId || teamId;
|
|
181
190
|
const workspaceRecipeId = await pickRecipeId({
|
|
@@ -192,7 +201,6 @@ export async function handleScaffoldTeam(
|
|
|
192
201
|
`Workspace recipe already exists: recipes/${id}.md. Choose --recipe-id (e.g. ${suggestions.join(", ")}) or --auto-increment or --overwrite-recipe.`,
|
|
193
202
|
});
|
|
194
203
|
await writeWorkspaceRecipeFile(loaded, recipesDir, workspaceRecipeId, overwriteRecipe);
|
|
195
|
-
|
|
196
204
|
const rolesDir = path.join(teamDir, "roles");
|
|
197
205
|
const notesDir = path.join(teamDir, "notes");
|
|
198
206
|
const workDir = path.join(teamDir, "work");
|
|
@@ -200,9 +208,17 @@ export async function handleScaffoldTeam(
|
|
|
200
208
|
const sharedContextDir = path.join(teamDir, "shared-context");
|
|
201
209
|
const goalsDir = path.join(notesDir, "goals");
|
|
202
210
|
|
|
211
|
+
const qaChecklist = Boolean(recipe.qaChecklist ?? false) || (recipe.agents ?? []).some((a) => String(a.role ?? "").toLowerCase() === "test");
|
|
203
212
|
await ensureTeamDirectoryStructure(teamDir, sharedContextDir, notesDir, workDir);
|
|
204
|
-
await writeTeamBootstrapFiles(
|
|
205
|
-
|
|
213
|
+
await writeTeamBootstrapFiles({
|
|
214
|
+
teamId,
|
|
215
|
+
teamDir,
|
|
216
|
+
sharedContextDir,
|
|
217
|
+
notesDir,
|
|
218
|
+
goalsDir,
|
|
219
|
+
overwrite,
|
|
220
|
+
qaChecklist,
|
|
221
|
+
});
|
|
206
222
|
const results = await scaffoldTeamAgents(api, recipe, teamId, teamDir, rolesDir, overwrite);
|
|
207
223
|
await writeTeamMetadataAndConfig({ api, teamId, teamDir, recipe, results, applyConfig: !!options.applyConfig, overwrite });
|
|
208
224
|
|
|
@@ -365,6 +381,23 @@ export async function handleRemoveTeam(
|
|
|
365
381
|
const cronJobsPath = path.resolve(workspaceRoot, "..", "cron", "jobs.json");
|
|
366
382
|
const cfgObj = await loadOpenClawConfig(api);
|
|
367
383
|
const cronStore = await loadCronStore(cronJobsPath);
|
|
384
|
+
|
|
385
|
+
// IMPORTANT: read cron provenance BEFORE deleting workspace.
|
|
386
|
+
// Teams/recipes track installed cron jobs via notes/cron-jobs.json.
|
|
387
|
+
const teamDir = path.resolve(workspaceRoot, "..", `workspace-${teamId}`);
|
|
388
|
+
const cronMappingPath = path.join(teamDir, "notes", "cron-jobs.json");
|
|
389
|
+
let installedCronIds: string[] = [];
|
|
390
|
+
try {
|
|
391
|
+
const raw = await fs.readFile(cronMappingPath, "utf8");
|
|
392
|
+
const json = JSON.parse(raw) as { entries?: Record<string, { installedCronId?: unknown; orphaned?: unknown }> };
|
|
393
|
+
installedCronIds = Object.values(json.entries ?? {})
|
|
394
|
+
.filter((e) => e && !e.orphaned)
|
|
395
|
+
.map((e) => String(e.installedCronId ?? "").trim())
|
|
396
|
+
.filter(Boolean);
|
|
397
|
+
} catch {
|
|
398
|
+
installedCronIds = [];
|
|
399
|
+
}
|
|
400
|
+
|
|
368
401
|
const plan = await buildRemoveTeamPlan({
|
|
369
402
|
teamId,
|
|
370
403
|
workspaceRoot,
|
|
@@ -372,6 +405,7 @@ export async function handleRemoveTeam(
|
|
|
372
405
|
cronJobsPath,
|
|
373
406
|
cfgObj,
|
|
374
407
|
cronStore,
|
|
408
|
+
installedCronIds,
|
|
375
409
|
});
|
|
376
410
|
if (options.plan) return { ok: true as const, plan };
|
|
377
411
|
if (!options.yes && !process.stdin.isTTY) {
|
package/src/lib/agent-config.ts
CHANGED
|
@@ -16,7 +16,17 @@ export function upsertAgentInConfig(cfgObj: AgentsConfigMutable, snippet: AgentC
|
|
|
16
16
|
const list = cfgObj.agents.list;
|
|
17
17
|
const idx = list.findIndex((a) => a?.id === snippet.id);
|
|
18
18
|
const prev = idx >= 0 ? list[idx] : {};
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
const prevTools = (() => {
|
|
21
|
+
const v = (prev as { tools?: unknown } | null | undefined)?.tools;
|
|
22
|
+
if (!v || typeof v !== "object") return undefined;
|
|
23
|
+
const t = v as { profile?: unknown; allow?: unknown; deny?: unknown };
|
|
24
|
+
return {
|
|
25
|
+
profile: typeof t.profile === "string" ? t.profile : undefined,
|
|
26
|
+
allow: Array.isArray(t.allow) ? (t.allow as string[]) : undefined,
|
|
27
|
+
deny: Array.isArray(t.deny) ? (t.deny as string[]) : undefined,
|
|
28
|
+
} as { profile?: string; allow?: string[]; deny?: string[] };
|
|
29
|
+
})();
|
|
20
30
|
const nextTools =
|
|
21
31
|
snippet.tools === undefined
|
|
22
32
|
? prevTools
|
|
@@ -34,6 +34,13 @@ export type RecipeFrontmatter = {
|
|
|
34
34
|
kind?: string;
|
|
35
35
|
name?: string;
|
|
36
36
|
cronJobs?: CronJobSpec[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* If true, scaffold `notes/QA_CHECKLIST.md` even if the team has no `test` role.
|
|
40
|
+
* If false/omitted, QA checklist is scaffolded only when the team recipe includes a `test` role.
|
|
41
|
+
*/
|
|
42
|
+
qaChecklist?: boolean;
|
|
43
|
+
|
|
37
44
|
[k: string]: unknown;
|
|
38
45
|
};
|
|
39
46
|
|
package/src/lib/remove-team.ts
CHANGED
|
@@ -81,22 +81,34 @@ export function findAgentsToRemove(cfgObj: Record<string, unknown>, teamId: stri
|
|
|
81
81
|
.filter((id: string) => id && id.startsWith(prefix));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function planCronJobRemovals(
|
|
84
|
+
export function planCronJobRemovals(
|
|
85
|
+
jobs: CronJob[],
|
|
86
|
+
teamId: string,
|
|
87
|
+
opts?: { installedCronIds?: string[] | null }
|
|
88
|
+
) {
|
|
85
89
|
const stamp = stampTeamId(teamId);
|
|
86
90
|
const exact: Array<{ id: string; name?: string }> = [];
|
|
87
91
|
const ambiguous: Array<{ id: string; name?: string; reason: string }> = [];
|
|
88
92
|
|
|
93
|
+
const installed = new Set((opts?.installedCronIds ?? []).map((s) => String(s).trim()).filter(Boolean));
|
|
94
|
+
|
|
89
95
|
for (const j of jobs) {
|
|
90
96
|
const msg = String(j?.payload?.message ?? "");
|
|
91
97
|
const name = String(j?.name ?? "");
|
|
92
98
|
|
|
93
|
-
// Exact:
|
|
99
|
+
// Exact (preferred): installedCronIds from the team provenance file.
|
|
100
|
+
if (installed.has(String(j.id))) {
|
|
101
|
+
exact.push({ id: j.id, name: j.name });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback exact: message contains the stamp.
|
|
94
106
|
if (msg.includes(stamp)) {
|
|
95
107
|
exact.push({ id: j.id, name: j.name });
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
// Ambiguous: name mentions teamId (helpful for manual review).
|
|
111
|
+
// Ambiguous: name/message mentions teamId (helpful for manual review).
|
|
100
112
|
if (name.includes(teamId) || msg.includes(teamId)) {
|
|
101
113
|
ambiguous.push({ id: j.id, name: j.name, reason: "mentions-teamId" });
|
|
102
114
|
}
|
|
@@ -112,6 +124,7 @@ export async function buildRemoveTeamPlan(opts: {
|
|
|
112
124
|
cronJobsPath: string; // e.g. ~/.openclaw/cron/jobs.json
|
|
113
125
|
cfgObj: Record<string, unknown>;
|
|
114
126
|
cronStore?: CronStore | null;
|
|
127
|
+
installedCronIds?: string[] | null;
|
|
115
128
|
}) {
|
|
116
129
|
const teamId = opts.teamId.trim();
|
|
117
130
|
const workspaceDir = path.resolve(path.join(opts.workspaceRoot, "..", `workspace-${teamId}`));
|
|
@@ -122,7 +135,7 @@ export async function buildRemoveTeamPlan(opts: {
|
|
|
122
135
|
const agentsToRemove = findAgentsToRemove(opts.cfgObj, teamId);
|
|
123
136
|
|
|
124
137
|
const jobs = (opts.cronStore?.jobs ?? []) as CronJob[];
|
|
125
|
-
const cron = planCronJobRemovals(jobs, teamId);
|
|
138
|
+
const cron = planCronJobRemovals(jobs, teamId, { installedCronIds: opts.installedCronIds });
|
|
126
139
|
|
|
127
140
|
const plan: RemoveTeamPlan = {
|
|
128
141
|
teamId,
|
package/src/lib/workspace.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
4
|
import { ensureDir } from "./fs-utils";
|
|
4
5
|
import { ticketStageDir } from "./lanes";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the OpenClaw workspace root.
|
|
9
|
+
*
|
|
10
|
+
* Priority:
|
|
11
|
+
* 1) config: agents.defaults.workspace
|
|
12
|
+
* 2) env: OPENCLAW_WORKSPACE
|
|
13
|
+
* 3) default: ~/.openclaw/workspace
|
|
14
|
+
*/
|
|
6
15
|
export function resolveWorkspaceRoot(api: OpenClawPluginApi): string {
|
|
7
16
|
const root = api.config.agents?.defaults?.workspace;
|
|
8
|
-
if (
|
|
9
|
-
|
|
17
|
+
if (root) return root;
|
|
18
|
+
|
|
19
|
+
const envRoot = process.env.OPENCLAW_WORKSPACE;
|
|
20
|
+
if (envRoot) return envRoot;
|
|
21
|
+
|
|
22
|
+
return path.join(os.homedir(), ".openclaw", "workspace");
|
|
10
23
|
}
|
|
11
24
|
|
|
12
25
|
export function resolveTeamDir(api: OpenClawPluginApi, teamId: string): string {
|