@clawcipes/recipes 0.2.2 → 0.2.3
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/docs/COMMANDS.md +7 -0
- package/docs/RECIPE_FORMAT.md +20 -0
- package/docs/shared-context.md +47 -0
- package/index.ts +327 -4
- package/package.json +1 -1
- package/recipes/default/development-team.md +91 -2
package/docs/COMMANDS.md
CHANGED
|
@@ -45,6 +45,13 @@ Options:
|
|
|
45
45
|
- `--apply-config` (write/update `agents.list[]` in OpenClaw config)
|
|
46
46
|
|
|
47
47
|
## `scaffold-team <recipeId>`
|
|
48
|
+
|
|
49
|
+
### Cron installation config
|
|
50
|
+
If a recipe declares `cronJobs`, scaffold will reconcile those jobs using the plugin config key:
|
|
51
|
+
- `plugins.entries.recipes.config.cronInstallation`: `off | prompt | on`
|
|
52
|
+
- `off`: never install/reconcile
|
|
53
|
+
- `prompt` (default): prompt each run (default answer is **No**)
|
|
54
|
+
- `on`: install/reconcile; new jobs follow `enabledByDefault`
|
|
48
55
|
Scaffold a shared **team workspace** + multiple agents from a **team** recipe.
|
|
49
56
|
|
|
50
57
|
```bash
|
package/docs/RECIPE_FORMAT.md
CHANGED
|
@@ -85,6 +85,26 @@ For team recipes, file templates are namespaced by role:
|
|
|
85
85
|
|
|
86
86
|
If a `files[].template` key does not contain a `.`, Clawcipes prefixes it with `<role>.`.
|
|
87
87
|
|
|
88
|
+
## Cron jobs (optional)
|
|
89
|
+
Recipes can optionally declare cron jobs to be reconciled during `scaffold` / `scaffold-team`.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
cronJobs:
|
|
93
|
+
- id: daily-review
|
|
94
|
+
schedule: "0 14 * * 1-5" # 5-field cron
|
|
95
|
+
message: "Daily review: summarize inbox + calendar for today."
|
|
96
|
+
enabledByDefault: false
|
|
97
|
+
timezone: "America/New_York" # optional
|
|
98
|
+
channel: "telegram" # optional (default: last)
|
|
99
|
+
to: "<chatId or phone>" # optional
|
|
100
|
+
description: "Weekday daily review"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Notes:
|
|
104
|
+
- `cronJobs[].id` must be **stable** within the recipe; it’s used for idempotent updates.
|
|
105
|
+
- Safe default behavior is conservative: when `cronInstallation=prompt`, the prompt default is **No**.
|
|
106
|
+
- When the user opts out, jobs are installed **disabled** (so they can be enabled later).
|
|
107
|
+
|
|
88
108
|
## Tool policy
|
|
89
109
|
Recipes can include tool policy, which is written into `agents.list[].tools` when `--apply-config` is used:
|
|
90
110
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Shared context conventions (team workspaces)
|
|
2
|
+
|
|
3
|
+
This is a **file-first** coordination convention for scaffolded teams.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
- Reduce cross-agent messaging; coordinate via shared files.
|
|
7
|
+
- Prevent shared context from becoming a junk drawer.
|
|
8
|
+
- Make work auditable: read → act → write loops.
|
|
9
|
+
|
|
10
|
+
## Directory layout
|
|
11
|
+
Team workspaces include:
|
|
12
|
+
|
|
13
|
+
- `notes/plan.md` — curated plan / priorities (lead-owned)
|
|
14
|
+
- `notes/status.md` — current snapshot (short, updated frequently)
|
|
15
|
+
- `shared-context/` — shared context + append-only inputs
|
|
16
|
+
- `priorities.md` — curated priorities (lead-owned)
|
|
17
|
+
- `agent-outputs/` — append-only outputs from non-lead roles
|
|
18
|
+
- `feedback/` — QA findings / feedback
|
|
19
|
+
- `kpis/` — metrics
|
|
20
|
+
- `calendar/` — optional notes
|
|
21
|
+
|
|
22
|
+
Back-compat:
|
|
23
|
+
- `shared/` may exist as a legacy folder. Prefer `shared-context/`.
|
|
24
|
+
|
|
25
|
+
## Curator model
|
|
26
|
+
- **Lead** curates:
|
|
27
|
+
- `notes/plan.md`
|
|
28
|
+
- `shared-context/priorities.md`
|
|
29
|
+
- **Non-lead roles** should not edit curated files.
|
|
30
|
+
- Append instead to `shared-context/agent-outputs/` (or `feedback/`).
|
|
31
|
+
|
|
32
|
+
## Read → Act → Write
|
|
33
|
+
Every role should:
|
|
34
|
+
1) **Read** plan/status + ticket before acting
|
|
35
|
+
2) **Act** (small, safe changes)
|
|
36
|
+
3) **Write back**
|
|
37
|
+
- update ticket
|
|
38
|
+
- add 3–5 bullets to `notes/status.md`
|
|
39
|
+
- append detailed outputs to `shared-context/agent-outputs/`
|
|
40
|
+
|
|
41
|
+
## Migration guidance (existing teams)
|
|
42
|
+
Safe migration:
|
|
43
|
+
1) Create `shared-context/` and starter subdirs.
|
|
44
|
+
2) Add `shared-context/priorities.md` (create-only; do not overwrite).
|
|
45
|
+
3) Update agent instructions to reference shared-context and curator model.
|
|
46
|
+
|
|
47
|
+
No existing files need to be deleted or renamed.
|
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import JSON5 from "json5";
|
|
5
6
|
import YAML from "yaml";
|
|
6
7
|
|
|
@@ -11,6 +12,32 @@ type RecipesConfig = {
|
|
|
11
12
|
workspaceTeamsDir?: string;
|
|
12
13
|
autoInstallMissingSkills?: boolean;
|
|
13
14
|
confirmAutoInstall?: boolean;
|
|
15
|
+
|
|
16
|
+
/** Cron installation behavior during scaffold/scaffold-team. */
|
|
17
|
+
cronInstallation?: "off" | "prompt" | "on";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type CronJobSpec = {
|
|
21
|
+
/** Stable id within the recipe (used for idempotent reconciliation). */
|
|
22
|
+
id: string;
|
|
23
|
+
/** 5-field cron expression */
|
|
24
|
+
schedule: string;
|
|
25
|
+
/** Agent message payload */
|
|
26
|
+
message: string;
|
|
27
|
+
|
|
28
|
+
name?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
timezone?: string;
|
|
31
|
+
|
|
32
|
+
/** Delivery routing (optional; defaults to OpenClaw "last"). */
|
|
33
|
+
channel?: string;
|
|
34
|
+
to?: string;
|
|
35
|
+
|
|
36
|
+
/** Which agent should execute this job (optional). */
|
|
37
|
+
agentId?: string;
|
|
38
|
+
|
|
39
|
+
/** If true, install enabled when cronInstallation=on (or prompt-yes). Default false. */
|
|
40
|
+
enabledByDefault?: boolean;
|
|
14
41
|
};
|
|
15
42
|
|
|
16
43
|
type RecipeFrontmatter = {
|
|
@@ -20,6 +47,9 @@ type RecipeFrontmatter = {
|
|
|
20
47
|
description?: string;
|
|
21
48
|
kind?: "agent" | "team";
|
|
22
49
|
|
|
50
|
+
/** Optional recipe-defined cron jobs to reconcile during scaffold. */
|
|
51
|
+
cronJobs?: CronJobSpec[];
|
|
52
|
+
|
|
23
53
|
// skill deps (installed into workspace-local skills dir)
|
|
24
54
|
requiredSkills?: string[];
|
|
25
55
|
optionalSkills?: string[];
|
|
@@ -71,6 +101,7 @@ function getCfg(api: OpenClawPluginApi): Required<RecipesConfig> {
|
|
|
71
101
|
workspaceTeamsDir: cfg.workspaceTeamsDir ?? "teams",
|
|
72
102
|
autoInstallMissingSkills: cfg.autoInstallMissingSkills ?? false,
|
|
73
103
|
confirmAutoInstall: cfg.confirmAutoInstall ?? true,
|
|
104
|
+
cronInstallation: cfg.cronInstallation ?? "prompt",
|
|
74
105
|
};
|
|
75
106
|
}
|
|
76
107
|
|
|
@@ -157,6 +188,267 @@ async function ensureDir(p: string) {
|
|
|
157
188
|
await fs.mkdir(p, { recursive: true });
|
|
158
189
|
}
|
|
159
190
|
|
|
191
|
+
type CronInstallMode = "off" | "prompt" | "on";
|
|
192
|
+
|
|
193
|
+
type CronMappingStateV1 = {
|
|
194
|
+
version: 1;
|
|
195
|
+
entries: Record<
|
|
196
|
+
string,
|
|
197
|
+
{
|
|
198
|
+
installedCronId: string;
|
|
199
|
+
specHash: string;
|
|
200
|
+
orphaned?: boolean;
|
|
201
|
+
updatedAtMs: number;
|
|
202
|
+
}
|
|
203
|
+
>;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function cronKey(scope: { kind: "team"; teamId: string; recipeId: string } | { kind: "agent"; agentId: string; recipeId: string }, cronJobId: string) {
|
|
207
|
+
return scope.kind === "team"
|
|
208
|
+
? `team:${scope.teamId}:recipe:${scope.recipeId}:cron:${cronJobId}`
|
|
209
|
+
: `agent:${scope.agentId}:recipe:${scope.recipeId}:cron:${cronJobId}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hashSpec(spec: unknown) {
|
|
213
|
+
const json = stableStringify(spec);
|
|
214
|
+
return crypto.createHash("sha256").update(json, "utf8").digest("hex");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readJsonFile<T>(p: string): Promise<T | null> {
|
|
218
|
+
try {
|
|
219
|
+
const raw = await fs.readFile(p, "utf8");
|
|
220
|
+
return JSON.parse(raw) as T;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function writeJsonFile(p: string, data: unknown) {
|
|
227
|
+
await ensureDir(path.dirname(p));
|
|
228
|
+
await fs.writeFile(p, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function loadCronMappingState(statePath: string): Promise<CronMappingStateV1> {
|
|
232
|
+
const existing = await readJsonFile<CronMappingStateV1>(statePath);
|
|
233
|
+
if (existing && existing.version === 1 && existing.entries && typeof existing.entries === "object") return existing;
|
|
234
|
+
return { version: 1, entries: {} };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type OpenClawCronJob = {
|
|
238
|
+
id: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
enabled?: boolean;
|
|
241
|
+
schedule?: any;
|
|
242
|
+
payload?: any;
|
|
243
|
+
delivery?: any;
|
|
244
|
+
agentId?: string;
|
|
245
|
+
description?: string;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function spawnOpenClawJson(args: string[]) {
|
|
249
|
+
const { spawnSync } = require("node:child_process") as typeof import("node:child_process");
|
|
250
|
+
const res = spawnSync("openclaw", args, { encoding: "utf8" });
|
|
251
|
+
if (res.status !== 0) {
|
|
252
|
+
const err = new Error(`openclaw ${args.join(" ")} failed (exit=${res.status})`);
|
|
253
|
+
(err as any).stdout = res.stdout;
|
|
254
|
+
(err as any).stderr = res.stderr;
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
const out = String(res.stdout ?? "").trim();
|
|
258
|
+
return out ? (JSON.parse(out) as any) : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeCronJobs(frontmatter: RecipeFrontmatter): CronJobSpec[] {
|
|
262
|
+
const raw = frontmatter.cronJobs;
|
|
263
|
+
if (!raw) return [];
|
|
264
|
+
if (!Array.isArray(raw)) throw new Error("frontmatter.cronJobs must be an array");
|
|
265
|
+
|
|
266
|
+
const out: CronJobSpec[] = [];
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
for (const j of raw as any[]) {
|
|
269
|
+
if (!j || typeof j !== "object") throw new Error("cronJobs entries must be objects");
|
|
270
|
+
const id = String((j as any).id ?? "").trim();
|
|
271
|
+
if (!id) throw new Error("cronJobs[].id is required");
|
|
272
|
+
if (seen.has(id)) throw new Error(`Duplicate cronJobs[].id: ${id}`);
|
|
273
|
+
seen.add(id);
|
|
274
|
+
|
|
275
|
+
const schedule = String((j as any).schedule ?? "").trim();
|
|
276
|
+
const message = String((j as any).message ?? (j as any).task ?? (j as any).prompt ?? "").trim();
|
|
277
|
+
if (!schedule) throw new Error(`cronJobs[${id}].schedule is required`);
|
|
278
|
+
if (!message) throw new Error(`cronJobs[${id}].message is required`);
|
|
279
|
+
|
|
280
|
+
out.push({
|
|
281
|
+
id,
|
|
282
|
+
schedule,
|
|
283
|
+
message,
|
|
284
|
+
name: (j as any).name ? String((j as any).name) : undefined,
|
|
285
|
+
description: (j as any).description ? String((j as any).description) : undefined,
|
|
286
|
+
timezone: (j as any).timezone ? String((j as any).timezone) : undefined,
|
|
287
|
+
channel: (j as any).channel ? String((j as any).channel) : undefined,
|
|
288
|
+
to: (j as any).to ? String((j as any).to) : undefined,
|
|
289
|
+
agentId: (j as any).agentId ? String((j as any).agentId) : undefined,
|
|
290
|
+
enabledByDefault: Boolean((j as any).enabledByDefault ?? false),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function promptYesNo(header: string) {
|
|
297
|
+
if (!process.stdin.isTTY) return false;
|
|
298
|
+
const readline = await import("node:readline/promises");
|
|
299
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
300
|
+
try {
|
|
301
|
+
const ans = await rl.question(`${header}\nProceed? (y/N) `);
|
|
302
|
+
return ans.trim().toLowerCase() === "y" || ans.trim().toLowerCase() === "yes";
|
|
303
|
+
} finally {
|
|
304
|
+
rl.close();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function reconcileRecipeCronJobs(opts: {
|
|
309
|
+
recipe: RecipeFrontmatter;
|
|
310
|
+
scope: { kind: "team"; teamId: string; recipeId: string; stateDir: string } | { kind: "agent"; agentId: string; recipeId: string; stateDir: string };
|
|
311
|
+
cronInstallation: CronInstallMode;
|
|
312
|
+
}) {
|
|
313
|
+
const desired = normalizeCronJobs(opts.recipe);
|
|
314
|
+
if (!desired.length) return { ok: true, changed: false, note: "no-cron-jobs" as const };
|
|
315
|
+
|
|
316
|
+
const mode = opts.cronInstallation;
|
|
317
|
+
if (mode === "off") {
|
|
318
|
+
return { ok: true, changed: false, note: "cron-installation-off" as const, desiredCount: desired.length };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Decide whether jobs should be enabled on creation. Default is conservative.
|
|
322
|
+
let userOptIn = mode === "on";
|
|
323
|
+
if (mode === "prompt") {
|
|
324
|
+
const header = `Recipe ${opts.scope.recipeId} defines ${desired.length} cron job(s).\nThese run automatically on a schedule. Install them?`;
|
|
325
|
+
userOptIn = await promptYesNo(header);
|
|
326
|
+
if (!userOptIn && !process.stdin.isTTY) {
|
|
327
|
+
console.error("Non-interactive mode: defaulting cron install to disabled.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const statePath = path.join(opts.scope.stateDir, "notes", "cron-jobs.json");
|
|
332
|
+
const state = await loadCronMappingState(statePath);
|
|
333
|
+
|
|
334
|
+
const list = spawnOpenClawJson(["cron", "list", "--json"]) as { jobs: OpenClawCronJob[] };
|
|
335
|
+
const byId = new Map((list?.jobs ?? []).map((j) => [j.id, j] as const));
|
|
336
|
+
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const desiredIds = new Set(desired.map((j) => j.id));
|
|
339
|
+
|
|
340
|
+
const results: any[] = [];
|
|
341
|
+
|
|
342
|
+
for (const j of desired) {
|
|
343
|
+
const key = cronKey(opts.scope as any, j.id);
|
|
344
|
+
const name = j.name ?? `${opts.scope.kind === "team" ? (opts.scope as any).teamId : (opts.scope as any).agentId} • ${opts.scope.recipeId} • ${j.id}`;
|
|
345
|
+
|
|
346
|
+
const desiredSpec = {
|
|
347
|
+
schedule: j.schedule,
|
|
348
|
+
message: j.message,
|
|
349
|
+
timezone: j.timezone ?? "",
|
|
350
|
+
channel: j.channel ?? "last",
|
|
351
|
+
to: j.to ?? "",
|
|
352
|
+
agentId: j.agentId ?? "",
|
|
353
|
+
name,
|
|
354
|
+
description: j.description ?? "",
|
|
355
|
+
};
|
|
356
|
+
const specHash = hashSpec(desiredSpec);
|
|
357
|
+
|
|
358
|
+
const prev = state.entries[key];
|
|
359
|
+
const installedId = prev?.installedCronId;
|
|
360
|
+
const existing = installedId ? byId.get(installedId) : undefined;
|
|
361
|
+
|
|
362
|
+
const wantEnabled = userOptIn ? Boolean(j.enabledByDefault) : false;
|
|
363
|
+
|
|
364
|
+
if (!existing) {
|
|
365
|
+
// Create new job.
|
|
366
|
+
const args = [
|
|
367
|
+
"cron",
|
|
368
|
+
"add",
|
|
369
|
+
"--json",
|
|
370
|
+
"--name",
|
|
371
|
+
name,
|
|
372
|
+
"--cron",
|
|
373
|
+
j.schedule,
|
|
374
|
+
"--message",
|
|
375
|
+
j.message,
|
|
376
|
+
"--announce",
|
|
377
|
+
];
|
|
378
|
+
if (!wantEnabled) args.push("--disabled");
|
|
379
|
+
if (j.description) args.push("--description", j.description);
|
|
380
|
+
if (j.timezone) args.push("--tz", j.timezone);
|
|
381
|
+
if (j.channel) args.push("--channel", j.channel);
|
|
382
|
+
if (j.to) args.push("--to", j.to);
|
|
383
|
+
if (j.agentId) args.push("--agent", j.agentId);
|
|
384
|
+
|
|
385
|
+
const created = spawnOpenClawJson(args) as any;
|
|
386
|
+
const newId = created?.id ?? created?.job?.id;
|
|
387
|
+
if (!newId) throw new Error("Failed to parse cron add output (missing id)");
|
|
388
|
+
|
|
389
|
+
state.entries[key] = { installedCronId: newId, specHash, updatedAtMs: now, orphaned: false };
|
|
390
|
+
results.push({ action: "created", key, installedCronId: newId, enabled: wantEnabled });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Update existing job if spec changed.
|
|
395
|
+
if (prev?.specHash !== specHash) {
|
|
396
|
+
const editArgs = [
|
|
397
|
+
"cron",
|
|
398
|
+
"edit",
|
|
399
|
+
existing.id,
|
|
400
|
+
"--name",
|
|
401
|
+
name,
|
|
402
|
+
"--cron",
|
|
403
|
+
j.schedule,
|
|
404
|
+
"--message",
|
|
405
|
+
j.message,
|
|
406
|
+
"--announce",
|
|
407
|
+
];
|
|
408
|
+
if (j.description) editArgs.push("--description", j.description);
|
|
409
|
+
if (j.timezone) editArgs.push("--tz", j.timezone);
|
|
410
|
+
if (j.channel) editArgs.push("--channel", j.channel);
|
|
411
|
+
if (j.to) editArgs.push("--to", j.to);
|
|
412
|
+
if (j.agentId) editArgs.push("--agent", j.agentId);
|
|
413
|
+
|
|
414
|
+
spawnOpenClawJson(editArgs);
|
|
415
|
+
results.push({ action: "updated", key, installedCronId: existing.id });
|
|
416
|
+
} else {
|
|
417
|
+
results.push({ action: "unchanged", key, installedCronId: existing.id });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Enabled precedence: if user did not opt in, force disabled. Otherwise preserve current enabled state.
|
|
421
|
+
if (!userOptIn) {
|
|
422
|
+
if (existing.enabled) {
|
|
423
|
+
spawnOpenClawJson(["cron", "edit", existing.id, "--disable"]);
|
|
424
|
+
results.push({ action: "disabled", key, installedCronId: existing.id });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
state.entries[key] = { installedCronId: existing.id, specHash, updatedAtMs: now, orphaned: false };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Handle removed jobs: disable safely.
|
|
432
|
+
for (const [key, entry] of Object.entries(state.entries)) {
|
|
433
|
+
if (!key.includes(`:recipe:${opts.scope.recipeId}:cron:`)) continue;
|
|
434
|
+
const cronId = key.split(":cron:")[1] ?? "";
|
|
435
|
+
if (!cronId || desiredIds.has(cronId)) continue;
|
|
436
|
+
|
|
437
|
+
const job = byId.get(entry.installedCronId);
|
|
438
|
+
if (job && job.enabled) {
|
|
439
|
+
spawnOpenClawJson(["cron", "edit", job.id, "--disable"]);
|
|
440
|
+
results.push({ action: "disabled-removed", key, installedCronId: job.id });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
state.entries[key] = { ...entry, orphaned: true, updatedAtMs: now };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await writeJsonFile(statePath, state);
|
|
447
|
+
|
|
448
|
+
const changed = results.some((r) => r.action === "created" || r.action === "updated" || r.action?.startsWith("disabled"));
|
|
449
|
+
return { ok: true, changed, results };
|
|
450
|
+
}
|
|
451
|
+
|
|
160
452
|
function renderTemplate(raw: string, vars: Record<string, string>) {
|
|
161
453
|
// Tiny, safe template renderer: replaces {{key}}.
|
|
162
454
|
// No conditionals, no eval.
|
|
@@ -1431,7 +1723,13 @@ const recipesPlugin = {
|
|
|
1431
1723
|
await applyAgentSnippetsToOpenClawConfig(api, [result.next.configSnippet]);
|
|
1432
1724
|
}
|
|
1433
1725
|
|
|
1434
|
-
|
|
1726
|
+
const cron = await reconcileRecipeCronJobs({
|
|
1727
|
+
recipe,
|
|
1728
|
+
scope: { kind: "agent", agentId: String(options.agentId), recipeId: recipe.id, stateDir: resolvedWorkspaceRoot },
|
|
1729
|
+
cronInstallation: getCfg(api).cronInstallation,
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
console.log(JSON.stringify({ ...result, cron }, null, 2));
|
|
1435
1733
|
});
|
|
1436
1734
|
|
|
1437
1735
|
cmd
|
|
@@ -1477,8 +1775,23 @@ const recipesPlugin = {
|
|
|
1477
1775
|
const doneDir = path.join(workDir, "done");
|
|
1478
1776
|
const assignmentsDir = path.join(workDir, "assignments");
|
|
1479
1777
|
|
|
1778
|
+
// Seed standard team files (createOnly unless --overwrite)
|
|
1779
|
+
const overwrite = !!options.overwrite;
|
|
1780
|
+
|
|
1781
|
+
const sharedContextDir = path.join(teamDir, "shared-context");
|
|
1782
|
+
const sharedContextOutputsDir = path.join(sharedContextDir, "agent-outputs");
|
|
1783
|
+
const sharedContextFeedbackDir = path.join(sharedContextDir, "feedback");
|
|
1784
|
+
const sharedContextKpisDir = path.join(sharedContextDir, "kpis");
|
|
1785
|
+
const sharedContextCalendarDir = path.join(sharedContextDir, "calendar");
|
|
1786
|
+
|
|
1480
1787
|
await Promise.all([
|
|
1788
|
+
// Back-compat: keep existing shared/ folder, but shared-context/ is canonical going forward.
|
|
1481
1789
|
ensureDir(path.join(teamDir, "shared")),
|
|
1790
|
+
ensureDir(sharedContextDir),
|
|
1791
|
+
ensureDir(sharedContextOutputsDir),
|
|
1792
|
+
ensureDir(sharedContextFeedbackDir),
|
|
1793
|
+
ensureDir(sharedContextKpisDir),
|
|
1794
|
+
ensureDir(sharedContextCalendarDir),
|
|
1482
1795
|
ensureDir(path.join(teamDir, "inbox")),
|
|
1483
1796
|
ensureDir(path.join(teamDir, "outbox")),
|
|
1484
1797
|
ensureDir(notesDir),
|
|
@@ -1489,8 +1802,11 @@ const recipesPlugin = {
|
|
|
1489
1802
|
ensureDir(assignmentsDir),
|
|
1490
1803
|
]);
|
|
1491
1804
|
|
|
1492
|
-
// Seed
|
|
1493
|
-
const
|
|
1805
|
+
// Seed shared-context starter schema (createOnly unless --overwrite)
|
|
1806
|
+
const sharedPrioritiesPath = path.join(sharedContextDir, "priorities.md");
|
|
1807
|
+
const prioritiesMd = `# Priorities — ${teamId}\n\n- (empty)\n\n## Notes\n- Lead curates this file.\n- Non-lead roles should append updates to shared-context/agent-outputs/ instead.\n`;
|
|
1808
|
+
await writeFileSafely(sharedPrioritiesPath, prioritiesMd, overwrite ? "overwrite" : "createOnly");
|
|
1809
|
+
|
|
1494
1810
|
const planPath = path.join(notesDir, "plan.md");
|
|
1495
1811
|
const statusPath = path.join(notesDir, "status.md");
|
|
1496
1812
|
const ticketsPath = path.join(teamDir, "TICKETS.md");
|
|
@@ -1550,7 +1866,7 @@ const recipesPlugin = {
|
|
|
1550
1866
|
|
|
1551
1867
|
// Create a minimal TEAM.md
|
|
1552
1868
|
const teamMdPath = path.join(teamDir, "TEAM.md");
|
|
1553
|
-
const teamMd = `# ${teamId}\n\nShared workspace for this agent team.\n\n## Folders\n- inbox/ — requests\n- outbox/ — deliverables\n- shared/ — shared artifacts\n- notes/ —
|
|
1869
|
+
const teamMd = `# ${teamId}\n\nShared workspace for this agent team.\n\n## Folders\n- inbox/ — requests\n- outbox/ — deliverables\n- shared-context/ — curated shared context + append-only agent outputs\n- shared/ — legacy shared artifacts (back-compat)\n- notes/ — plan + status\n- work/ — working files\n`;
|
|
1554
1870
|
await writeFileSafely(teamMdPath, teamMd, options.overwrite ? "overwrite" : "createOnly");
|
|
1555
1871
|
|
|
1556
1872
|
if (options.applyConfig) {
|
|
@@ -1558,12 +1874,19 @@ const recipesPlugin = {
|
|
|
1558
1874
|
await applyAgentSnippetsToOpenClawConfig(api, snippets);
|
|
1559
1875
|
}
|
|
1560
1876
|
|
|
1877
|
+
const cron = await reconcileRecipeCronJobs({
|
|
1878
|
+
recipe,
|
|
1879
|
+
scope: { kind: "team", teamId, recipeId: recipe.id, stateDir: teamDir },
|
|
1880
|
+
cronInstallation: getCfg(api).cronInstallation,
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1561
1883
|
console.log(
|
|
1562
1884
|
JSON.stringify(
|
|
1563
1885
|
{
|
|
1564
1886
|
teamId,
|
|
1565
1887
|
teamDir,
|
|
1566
1888
|
agents: results,
|
|
1889
|
+
cron,
|
|
1567
1890
|
next: {
|
|
1568
1891
|
note:
|
|
1569
1892
|
options.applyConfig
|
package/package.json
CHANGED
|
@@ -51,6 +51,32 @@ templates:
|
|
|
51
51
|
Team: {{teamId}}
|
|
52
52
|
Shared workspace: {{teamDir}}
|
|
53
53
|
|
|
54
|
+
## Guardrails (read → act → write)
|
|
55
|
+
|
|
56
|
+
Before you act:
|
|
57
|
+
1) Read:
|
|
58
|
+
- `notes/plan.md`
|
|
59
|
+
- `notes/status.md`
|
|
60
|
+
- `shared-context/priorities.md`
|
|
61
|
+
- the relevant ticket(s)
|
|
62
|
+
|
|
63
|
+
After you act:
|
|
64
|
+
1) Write back:
|
|
65
|
+
- Update tickets with decisions/assignments.
|
|
66
|
+
- Keep `notes/status.md` current (3–5 bullets per active ticket).
|
|
67
|
+
|
|
68
|
+
## Curator model
|
|
69
|
+
|
|
70
|
+
You are the curator of:
|
|
71
|
+
- `notes/plan.md`
|
|
72
|
+
- `shared-context/priorities.md`
|
|
73
|
+
|
|
74
|
+
Everyone else should append to:
|
|
75
|
+
- `shared-context/agent-outputs/` (append-only)
|
|
76
|
+
- `shared-context/feedback/`
|
|
77
|
+
|
|
78
|
+
Your job is to periodically distill those inputs into the curated files.
|
|
79
|
+
|
|
54
80
|
## File-first workflow (tickets)
|
|
55
81
|
|
|
56
82
|
Source of truth is the shared team workspace.
|
|
@@ -61,8 +87,9 @@ templates:
|
|
|
61
87
|
- `work/in-progress/` — tickets currently being executed
|
|
62
88
|
- `work/testing/` — tickets awaiting QA verification
|
|
63
89
|
- `work/done/` — completed tickets + completion notes
|
|
64
|
-
- `notes/plan.md` — current plan / priorities
|
|
90
|
+
- `notes/plan.md` — current plan / priorities (curated)
|
|
65
91
|
- `notes/status.md` — current status snapshot
|
|
92
|
+
- `shared-context/` — shared context + append-only outputs
|
|
66
93
|
|
|
67
94
|
### Ticket numbering (critical)
|
|
68
95
|
- Backlog tickets MUST be named `0001-...md`, `0002-...md`, etc.
|
|
@@ -78,7 +105,8 @@ templates:
|
|
|
78
105
|
|
|
79
106
|
### Your responsibilities
|
|
80
107
|
- For every new request in `inbox/`, create a normalized ticket in `work/backlog/`.
|
|
81
|
-
-
|
|
108
|
+
- Curate `notes/plan.md` and `shared-context/priorities.md`.
|
|
109
|
+
- Keep `notes/status.md` updated.
|
|
82
110
|
- When work is ready for QA, move the ticket to `work/testing/` and assign it to the tester.
|
|
83
111
|
- Only after QA verification, move the ticket to `work/done/` (or use `openclaw recipes complete`).
|
|
84
112
|
- When a completion appears in `work/done/`, write a short summary into `outbox/`.
|
|
@@ -94,6 +122,29 @@ templates:
|
|
|
94
122
|
|
|
95
123
|
Shared workspace: {{teamDir}}
|
|
96
124
|
|
|
125
|
+
## Guardrails (read → act → write)
|
|
126
|
+
|
|
127
|
+
Before you change anything:
|
|
128
|
+
1) Read:
|
|
129
|
+
- `notes/plan.md`
|
|
130
|
+
- `notes/status.md`
|
|
131
|
+
- `shared-context/priorities.md`
|
|
132
|
+
- the current ticket you’re working on
|
|
133
|
+
|
|
134
|
+
While working:
|
|
135
|
+
- Keep changes small and safe.
|
|
136
|
+
- Prefer file-first coordination over chat.
|
|
137
|
+
|
|
138
|
+
After you finish a work session (even if not done):
|
|
139
|
+
1) Write back:
|
|
140
|
+
- Update the ticket with what you did and what’s next.
|
|
141
|
+
- Add **3–5 bullets** to `notes/status.md` (what changed / what’s blocked).
|
|
142
|
+
- Append detailed output to `shared-context/agent-outputs/` (append-only).
|
|
143
|
+
|
|
144
|
+
Curator model:
|
|
145
|
+
- Lead curates `notes/plan.md` and `shared-context/priorities.md`.
|
|
146
|
+
- You should NOT edit curated files; propose changes via `agent-outputs/`.
|
|
147
|
+
|
|
97
148
|
## How you work (pull system)
|
|
98
149
|
|
|
99
150
|
1) Look in `work/in-progress/` for any ticket already assigned to you.
|
|
@@ -122,6 +173,25 @@ templates:
|
|
|
122
173
|
|
|
123
174
|
Shared workspace: {{teamDir}}
|
|
124
175
|
|
|
176
|
+
## Guardrails (read → act → write)
|
|
177
|
+
|
|
178
|
+
Before you change anything:
|
|
179
|
+
1) Read:
|
|
180
|
+
- `notes/plan.md`
|
|
181
|
+
- `notes/status.md`
|
|
182
|
+
- `shared-context/priorities.md`
|
|
183
|
+
- the current ticket you’re working on
|
|
184
|
+
|
|
185
|
+
After you finish a work session:
|
|
186
|
+
1) Write back:
|
|
187
|
+
- Update the ticket with what you did + verification steps.
|
|
188
|
+
- Add **3–5 bullets** to `notes/status.md`.
|
|
189
|
+
- Append detailed output/logs to `shared-context/agent-outputs/` (append-only).
|
|
190
|
+
|
|
191
|
+
Curator model:
|
|
192
|
+
- Lead curates `notes/plan.md` and `shared-context/priorities.md`.
|
|
193
|
+
- You should NOT edit curated files; propose changes via `agent-outputs/`.
|
|
194
|
+
|
|
125
195
|
## How you work (pull system)
|
|
126
196
|
|
|
127
197
|
1) Look in `work/in-progress/` for any ticket already assigned to you.
|
|
@@ -199,6 +269,25 @@ templates:
|
|
|
199
269
|
|
|
200
270
|
Shared workspace: {{teamDir}}
|
|
201
271
|
|
|
272
|
+
## Guardrails (read → act → write)
|
|
273
|
+
|
|
274
|
+
Before verifying:
|
|
275
|
+
1) Read:
|
|
276
|
+
- `notes/plan.md`
|
|
277
|
+
- `notes/status.md`
|
|
278
|
+
- `shared-context/priorities.md`
|
|
279
|
+
- the ticket under test
|
|
280
|
+
|
|
281
|
+
After each verification pass:
|
|
282
|
+
1) Write back:
|
|
283
|
+
- Add a short verification note to the ticket (pass/fail + evidence).
|
|
284
|
+
- Add **3–5 bullets** to `notes/status.md` (what’s verified / what’s blocked).
|
|
285
|
+
- Append detailed findings to `shared-context/feedback/` or `shared-context/agent-outputs/`.
|
|
286
|
+
|
|
287
|
+
Curator model:
|
|
288
|
+
- Lead curates `notes/plan.md` and `shared-context/priorities.md`.
|
|
289
|
+
- You should NOT edit curated files; propose changes via feedback/outputs.
|
|
290
|
+
|
|
202
291
|
## How you work
|
|
203
292
|
|
|
204
293
|
1) Look in `work/testing/` for tickets assigned to you.
|