@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 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
@@ -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
- console.log(JSON.stringify(result, null, 2));
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 standard team files (createOnly unless --overwrite)
1493
- const overwrite = !!options.overwrite;
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/ — notes\n- work/ — working files\n`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcipes/recipes",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Clawcipes recipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -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
- - Update `notes/plan.md` and `notes/status.md`.
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.