@jiggai/recipes 0.4.19 → 0.4.20

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.
@@ -0,0 +1,23 @@
1
+ # Workflow fixes (2026-03-13)
2
+
3
+ This note summarizes the workflow reliability fixes merged on 2026-03-13.
4
+
5
+ ## Kitchen (workflow editor)
6
+ - Persist real approval binding ids when saving workflows.
7
+ - Prevents saving synthetic ids like `telegram:dm:<peer>`.
8
+ - Ensures `meta.approvalBindingId` and the `human_approval` node config stay in sync.
9
+
10
+ ## Recipes / workflow runner
11
+ ### Approval ingestion
12
+ - Robust parsing of approval replies (plain text and formatted/wrapped text).
13
+ - Use the runtime hook name (`message_received`) and scan the canonical workspace root so approval lookup works even when the inbound message is routed to a role workspace (e.g. `workspace-<teamId>/roles/copywriter`).
14
+
15
+ ### Queue / worker durability
16
+ - Skip stale recovered tasks so expired claim recovery cannot replay old nodes on runs that already advanced.
17
+
18
+ ### Worker cron execution
19
+ - Removed `tools.deny: ["exec"]` from the default `marketing-team` recipe so isolated worker loops can actually execute `worker-tick` commands.
20
+
21
+ ## Known remaining issues / follow-ups
22
+ - Completion Telegram notifier is still somewhat intermittent. When it fires, it includes the X URL and runId.
23
+ - If it remains flaky, likely fix is to add a deterministic notification node (workflow-level) or a runner-level `run.completed` notifier that uses the `post_to_platforms` artifact URLs.
package/index.ts CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  executeWorkspaceCleanup,
53
53
  planWorkspaceCleanup,
54
54
  } from "./src/lib/cleanup-workspaces";
55
- import { resolveWorkspaceRoot } from "./src/lib/workspace";
55
+ import { resolveCanonicalWorkspaceRoot, resolveWorkspaceRoot } from "./src/lib/workspace";
56
56
 
57
57
  function isRecord(v: unknown): v is Record<string, unknown> {
58
58
  return !!v && typeof v === 'object' && !Array.isArray(v);
@@ -62,6 +62,73 @@ function asString(v: unknown, fallback = ''): string {
62
62
  return typeof v === 'string' ? v : (v == null ? fallback : String(v));
63
63
  }
64
64
 
65
+ function extractEventText(evt: Record<string, unknown>, ctx: Record<string, unknown>, metadata: Record<string, unknown>): string {
66
+ const msg = isRecord(evt["message"]) ? (evt["message"] as Record<string, unknown>) : {};
67
+ const parts = Array.isArray(msg["content"]) ? (msg["content"] as unknown[]) : [];
68
+ const texts = parts
69
+ .map((part) => (isRecord(part) ? asString(part["text"]).trim() : ""))
70
+ .filter(Boolean);
71
+ if (texts.length) return texts.join("\n").trim();
72
+
73
+ const direct = [
74
+ evt["content"],
75
+ ctx["content"],
76
+ evt["text"],
77
+ evt["body"],
78
+ metadata["content"],
79
+ metadata["text"],
80
+ metadata["message"],
81
+ ]
82
+ .map((v) => asString(v).trim())
83
+ .filter(Boolean);
84
+ if (direct.length) return direct.join("\n");
85
+ return "";
86
+ }
87
+
88
+ type ApprovalReply = {
89
+ approved: boolean;
90
+ code: string;
91
+ note?: string;
92
+ };
93
+
94
+ function parseApprovalReply(text: string): ApprovalReply | null {
95
+ const raw = String(text ?? '');
96
+ const lines = raw
97
+ .split(/\r?\n/)
98
+ .map((line) => line.trim())
99
+ .filter(Boolean)
100
+ .reverse();
101
+
102
+ for (const line of lines) {
103
+ const m = line.match(/\b(approve|decline)\b\s+([A-Z0-9]{4,8})(?:\s+(.+))?$/i);
104
+ if (!m) continue;
105
+ const verb = String(m[1] ?? '').toLowerCase();
106
+ const code = String(m[2] ?? '').toUpperCase();
107
+ const note = asString(m[3]).trim();
108
+ return {
109
+ approved: verb === 'approve',
110
+ code,
111
+ ...(note ? { note } : {}),
112
+ };
113
+ }
114
+
115
+ const fallback = raw.match(/\b(approve|decline)\b\s+([A-Z0-9]{4,8})(?:\s+(.+))?/i);
116
+ if (!fallback) return null;
117
+ const verb = String(fallback[1] ?? '').toLowerCase();
118
+ const code = String(fallback[2] ?? '').toUpperCase();
119
+ const note = asString(fallback[3]).trim();
120
+ return {
121
+ approved: verb === 'approve',
122
+ code,
123
+ ...(note ? { note } : {}),
124
+ };
125
+ }
126
+
127
+ function shouldProcessApprovalReply(channelHints: string[]): boolean {
128
+ if (!channelHints.length) return true;
129
+ return channelHints.some((v) => v.includes("telegram"));
130
+ }
131
+
65
132
  const recipesPlugin = {
66
133
  id: "recipes",
67
134
  name: "Recipes",
@@ -73,35 +140,60 @@ const recipesPlugin = {
73
140
  },
74
141
  register(api: OpenClawPluginApi) {
75
142
  // Auto-approval via chat reply (MVP):
76
- // If a human replies `approve <runId>` or `decline <runId>` in the bound channel,
143
+ // If a human replies `approve <code>` or `decline <code>` in the bound channel,
77
144
  // record the decision and resume the run.
78
- api.on(
79
- "message_received" as never,
80
- async (evt: unknown) => {
145
+ const approvalReplyHandler = async (evt: unknown, ctx: unknown) => {
81
146
  try {
82
147
  const e = isRecord(evt) ? evt : {};
83
- const channel = asString(e["messageProvider"] ?? e["channelId"] ?? e["channel"]);
84
- const text = asString(e["text"] ?? e["message"] ?? e["body"]).trim();
148
+ const c = isRecord(ctx) ? ctx : {};
149
+ const metadata = isRecord(e["metadata"]) ? (e["metadata"] as Record<string, unknown>) : {};
150
+ const text = extractEventText(e, c, metadata);
85
151
  if (!text) return;
86
152
 
87
- // Only enable for Telegram for now (matches RJ's request).
88
- if (channel !== "telegram") return;
153
+ const reply = parseApprovalReply(text);
154
+ if (!reply) return;
89
155
 
90
- const m = text.match(/^(approve|decline)\s+([A-Z0-9]{4,8})\s*$/i);
91
- if (!m) return;
92
- const verb = String(m[1] ?? "").toLowerCase();
93
- const code = String(m[2] ?? "").toUpperCase();
94
- const approved = verb === "approve";
95
-
96
- const workspaceRoot = resolveWorkspaceRoot(api);
156
+ const workspaceRoot = resolveCanonicalWorkspaceRoot(api);
97
157
  const parent = path.resolve(workspaceRoot, "..");
158
+ const roots = Array.from(new Set([parent, workspaceRoot, path.join(workspaceRoot, "workspace")]));
159
+ const channelHints = [
160
+ c["channelId"],
161
+ c["channel"],
162
+ metadata["channelId"],
163
+ metadata["channel"],
164
+ metadata["provider"],
165
+ metadata["source"],
166
+ e["messageProvider"],
167
+ e["channelId"],
168
+ e["channel"],
169
+ e["source"],
170
+ ]
171
+ .map((v) => asString(v).toLowerCase())
172
+ .filter(Boolean);
173
+ const isTelegram = shouldProcessApprovalReply(channelHints);
98
174
 
99
- // Scan workspace-*/shared-context/workflow-runs/*/approvals/approval.json for a matching code.
100
- const teamDirs = (await fs.readdir(parent, { withFileTypes: true }))
101
- .filter((d) => d.isDirectory() && d.name.startsWith("workspace-"))
102
- .map((d) => path.join(parent, d.name));
175
+ // Only reject when the event explicitly identifies a different channel.
176
+ // Some Telegram inbound payloads arrive without provider/channel metadata.
177
+ if (!isTelegram) {
178
+ console.error(`[recipes] approval reply ignored: non-telegram channel hints=${JSON.stringify(channelHints)} text=${JSON.stringify(text)}`);
179
+ return;
180
+ }
103
181
 
104
- let found: { teamId: string; runId: string } | null = null;
182
+ const { approved, code, note } = reply;
183
+
184
+ const teamDirs: string[] = [];
185
+ for (const root of roots) {
186
+ try {
187
+ const entries = await fs.readdir(root, { withFileTypes: true });
188
+ for (const d of entries) {
189
+ if (d.isDirectory() && d.name.startsWith("workspace-")) teamDirs.push(path.join(root, d.name));
190
+ }
191
+ } catch {
192
+ // ignore root read errors
193
+ }
194
+ }
195
+
196
+ let found: { teamId: string; runId: string; approvalPath: string } | null = null;
105
197
 
106
198
  for (const teamDir of teamDirs) {
107
199
  const teamId = path.basename(teamDir).replace(/^workspace-/, "");
@@ -120,8 +212,8 @@ const recipesPlugin = {
120
212
  try {
121
213
  const raw = await fs.readFile(approvalPath, "utf8");
122
214
  const a = JSON.parse(raw) as { code?: string; status?: string; runId?: string; teamId?: string };
123
- if (String(a?.code ?? "").toUpperCase() === code && String(a?.status ?? "") === "pending") {
124
- found = { teamId: String(a?.teamId ?? teamId), runId: String(a?.runId ?? runId) };
215
+ if (String(a?.code ?? "").trim().toUpperCase() === code && String(a?.status ?? "") === "pending") {
216
+ found = { teamId: String(a?.teamId ?? teamId), runId: String(a?.runId ?? runId), approvalPath };
125
217
  break;
126
218
  }
127
219
  } catch {
@@ -131,10 +223,14 @@ const recipesPlugin = {
131
223
  if (found) break;
132
224
  }
133
225
 
134
- if (!found) return;
226
+ if (!found) {
227
+ console.error(`[recipes] approval reply not matched: code=${code} text=${JSON.stringify(text)} hints=${JSON.stringify(channelHints)} roots=${JSON.stringify(roots)}`);
228
+ return;
229
+ }
135
230
 
136
- await handleWorkflowsApprove(api, { teamId: found.teamId, runId: found.runId, approved, note: `Approved via Telegram (${code})` });
137
- // Resume is best-effort: the worker may have already flipped the run status.
231
+ console.error(`[recipes] approval reply matched: code=${code} team=${found.teamId} run=${found.runId} path=${found.approvalPath} approved=${approved}`);
232
+ const approvalNote = note || `${approved ? 'Approved' : 'Declined'} via Telegram (${code})`;
233
+ await handleWorkflowsApprove(api, { teamId: found.teamId, runId: found.runId, approved, note: approvalNote });
138
234
  try {
139
235
  await handleWorkflowsResume(api, { teamId: found.teamId, runId: found.runId });
140
236
  } catch {
@@ -143,9 +239,11 @@ const recipesPlugin = {
143
239
  } catch (e) {
144
240
  console.error(`[recipes] approval reply handler error: ${(e as Error).message}`);
145
241
  }
146
- },
147
- { priority: 50 } as unknown as { priority: number }
148
- );
242
+ };
243
+
244
+ api.on("message_received" as never, approvalReplyHandler as never, { priority: 50 } as unknown as { priority: number });
245
+ api.on("message:received" as never, approvalReplyHandler as never, { priority: 50 } as unknown as { priority: number });
246
+
149
247
 
150
248
  // On plugin load, ensure multi-agent config has an explicit agents.list with main at top.
151
249
  // This is idempotent and only writes if a change is required.
@@ -933,6 +1031,9 @@ workflows
933
1031
 
934
1032
  // Internal helpers used by unit tests. Not part of the public plugin API.
935
1033
  export const __internal = {
1034
+ extractEventText,
1035
+ parseApprovalReply,
1036
+ shouldProcessApprovalReply,
936
1037
  ensureMainFirstInAgentsList,
937
1038
  upsertBindingInConfig,
938
1039
  removeBindingsInConfig,
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.19",
5
+ "version": "0.4.20",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.19",
3
+ "version": "0.4.20",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -68,16 +68,16 @@ templates:
68
68
 
69
69
  ## Where to write things
70
70
  - Ticket = source of truth for a unit of work.
71
- - `notes/plan.md` + `shared-context/priorities.md` are **lead-curated**.
72
- - `notes/status.md` is **append-only** and updated after each work session (3–5 bullets).
73
- - `shared-context/agent-outputs/` is **append-only** logs/output.
71
+ - `../notes/plan.md` + `../shared-context/priorities.md` are **lead-curated**.
72
+ - `../notes/status.md` is **append-only** and updated after each work session (3–5 bullets).
73
+ - `../shared-context/agent-outputs/` is **append-only** logs/output.
74
74
 
75
75
  ## End-of-session checklist (everyone)
76
76
  After meaningful work:
77
77
  1) Update the ticket with what changed + how to verify + rollback.
78
78
  2) Add a dated note in the ticket `## Comments`.
79
- 3) Append 3–5 bullets to `notes/status.md`.
80
- 4) Append logs/output to `shared-context/agent-outputs/`.
79
+ 3) Append 3–5 bullets to `../notes/status.md`.
80
+ 4) Append logs/output to `../shared-context/agent-outputs/`.
81
81
 
82
82
  sharedContext.plan: |
83
83
  # Plan (lead-curated)
@@ -110,16 +110,16 @@ templates:
110
110
  - `roles/<role>/memory/YYYY-MM-DD.md` (daily log)
111
111
 
112
112
  ## Plan vs status (team coordination)
113
- - `notes/plan.md` + `shared-context/priorities.md` are lead-curated
114
- - `notes/status.md` is append-only roll-up (everyone appends)
113
+ - `../notes/plan.md` + `../shared-context/priorities.md` are lead-curated
114
+ - `../notes/status.md` is append-only roll-up (everyone appends)
115
115
 
116
116
  ## Outputs / artifacts
117
117
  - `roles/<role>/agent-outputs/` (append-only)
118
- - `shared-context/agent-outputs/` (optional team-level)
118
+ - `../shared-context/agent-outputs/` (team-level, read/write from role via `../`)
119
119
 
120
120
  ## Role work loop contract (safe-idle)
121
121
  - No-op unless explicit queued work exists for the role.
122
- - If work happens, write back in order: ticket → `notes/status.md` → `roles/<role>/agent-outputs/`.
122
+ - If work happens, write back in order: ticket → `../notes/status.md` → `roles/<role>/agent-outputs/`.
123
123
 
124
124
  sharedContext.priorities: |
125
125
  # Priorities (lead-curated)
@@ -172,24 +172,24 @@ templates:
172
172
 
173
173
  Before you act:
174
174
  1) Read:
175
- - `notes/plan.md`
176
- - `notes/status.md`
177
- - `shared-context/priorities.md`
175
+ - `../notes/plan.md`
176
+ - `../notes/status.md`
177
+ - `../shared-context/priorities.md`
178
178
  - the relevant ticket(s)
179
179
 
180
180
  After you act:
181
181
  1) Write back:
182
182
  - Update tickets with decisions/assignments.
183
- - Keep `notes/status.md` current (3–5 bullets per active ticket).
183
+ - Keep `../notes/status.md` current (3–5 bullets per active ticket).
184
184
 
185
185
  ## Curator model
186
186
 
187
187
  You are the curator of:
188
- - `notes/plan.md`
189
- - `shared-context/priorities.md`
188
+ - `../notes/plan.md`
189
+ - `../shared-context/priorities.md`
190
190
 
191
191
  Everyone else should append to:
192
- - `shared-context/agent-outputs/` (append-only)
192
+ - `../shared-context/agent-outputs/` (append-only)
193
193
  - `shared-context/feedback/`
194
194
 
195
195
  Your job is to periodically distill those inputs into the curated files.
@@ -204,8 +204,8 @@ templates:
204
204
  - `work/in-progress/` — tickets currently being executed
205
205
  - `work/testing/` — tickets awaiting QA verification
206
206
  - `work/done/` — completed tickets + completion notes
207
- - `notes/plan.md` — current plan / priorities (curated)
208
- - `notes/status.md` — current status snapshot
207
+ - `../notes/plan.md` — current plan / priorities (curated)
208
+ - `../notes/status.md` — current status snapshot
209
209
  - `shared-context/` — shared context + append-only outputs
210
210
 
211
211
  ### Ticket numbering (critical)
@@ -222,8 +222,8 @@ templates:
222
222
 
223
223
  ### Your responsibilities
224
224
  - For every new request in `inbox/`, create a normalized ticket in `work/backlog/`.
225
- - Curate `notes/plan.md` and `shared-context/priorities.md`.
226
- - Keep `notes/status.md` updated.
225
+ - Curate `../notes/plan.md` and `../shared-context/priorities.md`.
226
+ - Keep `../notes/status.md` updated.
227
227
  - When work is ready for QA, move the ticket to `work/testing/` and assign it to the tester.
228
228
  - Only after QA verification, move the ticket to `work/done/` (or use `openclaw recipes complete`).
229
229
  - When a completion appears in `work/done/`, write a short summary into `outbox/`.
@@ -244,8 +244,8 @@ templates:
244
244
  ## Guardrails (read → act → write)
245
245
  Before you act:
246
246
  1) Read:
247
- - `notes/plan.md`
248
- - `notes/status.md`
247
+ - `../notes/plan.md`
248
+ - `../notes/status.md`
249
249
  - relevant ticket(s) in `work/in-progress/`
250
250
  - any relevant shared context under `shared-context/`
251
251
 
@@ -274,8 +274,8 @@ templates:
274
274
  ## Guardrails (read → act → write)
275
275
  Before you act:
276
276
  1) Read:
277
- - `notes/plan.md`
278
- - `notes/status.md`
277
+ - `../notes/plan.md`
278
+ - `../notes/status.md`
279
279
  - relevant ticket(s) in `work/in-progress/`
280
280
  - any relevant shared context under `shared-context/`
281
281
 
@@ -304,8 +304,8 @@ templates:
304
304
  ## Guardrails (read → act → write)
305
305
  Before you act:
306
306
  1) Read:
307
- - `notes/plan.md`
308
- - `notes/status.md`
307
+ - `../notes/plan.md`
308
+ - `../notes/status.md`
309
309
  - relevant ticket(s) in `work/in-progress/`
310
310
  - any relevant shared context under `shared-context/`
311
311
 
@@ -334,8 +334,8 @@ templates:
334
334
  ## Guardrails (read → act → write)
335
335
  Before you act:
336
336
  1) Read:
337
- - `notes/plan.md`
338
- - `notes/status.md`
337
+ - `../notes/plan.md`
338
+ - `../notes/status.md`
339
339
  - relevant ticket(s) in `work/in-progress/`
340
340
  - any relevant shared context under `shared-context/`
341
341
 
@@ -57,16 +57,16 @@ templates:
57
57
 
58
58
  ## Where to write things
59
59
  - Ticket = source of truth for a unit of work.
60
- - `notes/plan.md` + `shared-context/priorities.md` are **lead-curated**.
61
- - `notes/status.md` is **append-only** and updated after each work session (3–5 bullets).
62
- - `shared-context/agent-outputs/` is **append-only** logs/output.
60
+ - `../notes/plan.md` + `../shared-context/priorities.md` are **lead-curated**.
61
+ - `../notes/status.md` is **append-only** and updated after each work session (3–5 bullets).
62
+ - `../shared-context/agent-outputs/` is **append-only** logs/output.
63
63
 
64
64
  ## End-of-session checklist (everyone)
65
65
  After meaningful work:
66
66
  1) Update the ticket with what changed + how to verify + rollback.
67
67
  2) Add a dated note in the ticket `## Comments`.
68
- 3) Append 3–5 bullets to `notes/status.md`.
69
- 4) Append logs/output to `shared-context/agent-outputs/`.
68
+ 3) Append 3–5 bullets to `../notes/status.md`.
69
+ 4) Append logs/output to `../shared-context/agent-outputs/`.
70
70
 
71
71
  sharedContext.plan: |
72
72
  # Plan (lead-curated)
@@ -99,16 +99,16 @@ templates:
99
99
  - `roles/<role>/memory/YYYY-MM-DD.md` (daily log)
100
100
 
101
101
  ## Plan vs status (team coordination)
102
- - `notes/plan.md` + `shared-context/priorities.md` are lead-curated
103
- - `notes/status.md` is append-only roll-up (everyone appends)
102
+ - `../notes/plan.md` + `../shared-context/priorities.md` are lead-curated
103
+ - `../notes/status.md` is append-only roll-up (everyone appends)
104
104
 
105
105
  ## Outputs / artifacts
106
106
  - `roles/<role>/agent-outputs/` (append-only)
107
- - `shared-context/agent-outputs/` (optional team-level)
107
+ - `../shared-context/agent-outputs/` (team-level, read/write from role via `../`)
108
108
 
109
109
  ## Role work loop contract (safe-idle)
110
110
  - No-op unless explicit queued work exists for the role.
111
- - If work happens, write back in order: ticket → `notes/status.md` → `roles/<role>/agent-outputs/`.
111
+ - If work happens, write back in order: ticket → `../notes/status.md` → `roles/<role>/agent-outputs/`.
112
112
 
113
113
  sharedContext.priorities: |
114
114
  # Priorities (lead-curated)
@@ -141,24 +141,24 @@ templates:
141
141
 
142
142
  Before you act:
143
143
  1) Read:
144
- - `notes/plan.md`
145
- - `notes/status.md`
146
- - `shared-context/priorities.md`
144
+ - `../notes/plan.md`
145
+ - `../notes/status.md`
146
+ - `../shared-context/priorities.md`
147
147
  - the relevant ticket(s)
148
148
 
149
149
  After you act:
150
150
  1) Write back:
151
151
  - Update tickets with decisions/assignments.
152
- - Keep `notes/status.md` current (3–5 bullets per active ticket).
152
+ - Keep `../notes/status.md` current (3–5 bullets per active ticket).
153
153
 
154
154
  ## Curator model
155
155
 
156
156
  You are the curator of:
157
- - `notes/plan.md`
158
- - `shared-context/priorities.md`
157
+ - `../notes/plan.md`
158
+ - `../shared-context/priorities.md`
159
159
 
160
160
  Everyone else should append to:
161
- - `shared-context/agent-outputs/` (append-only)
161
+ - `../shared-context/agent-outputs/` (append-only)
162
162
  - `shared-context/feedback/`
163
163
 
164
164
  Your job is to periodically distill those inputs into the curated files.
@@ -173,8 +173,8 @@ templates:
173
173
  - `work/in-progress/` — tickets currently being executed
174
174
  - `work/testing/` — tickets awaiting QA verification
175
175
  - `work/done/` — completed tickets + completion notes
176
- - `notes/plan.md` — current plan / priorities (curated)
177
- - `notes/status.md` — current status snapshot
176
+ - `../notes/plan.md` — current plan / priorities (curated)
177
+ - `../notes/status.md` — current status snapshot
178
178
  - `shared-context/` — shared context + append-only outputs
179
179
 
180
180
  ### Ticket numbering (critical)
@@ -191,8 +191,8 @@ templates:
191
191
 
192
192
  ### Your responsibilities
193
193
  - For every new request in `inbox/`, create a normalized ticket in `work/backlog/`.
194
- - Curate `notes/plan.md` and `shared-context/priorities.md`.
195
- - Keep `notes/status.md` updated.
194
+ - Curate `../notes/plan.md` and `../shared-context/priorities.md`.
195
+ - Keep `../notes/status.md` updated.
196
196
  - When work is ready for QA, move the ticket to `work/testing/` and assign it to the tester.
197
197
  - Only after QA verification, move the ticket to `work/done/` (or use `openclaw recipes complete`).
198
198
  - When a completion appears in `work/done/`, write a short summary into `outbox/`.
@@ -216,8 +216,8 @@ templates:
216
216
  ## Guardrails (read → act → write)
217
217
  Before you act:
218
218
  1) Read:
219
- - `notes/plan.md`
220
- - `notes/status.md`
219
+ - `../notes/plan.md`
220
+ - `../notes/status.md`
221
221
  - relevant ticket(s) in `work/in-progress/`
222
222
  - any relevant shared context under `shared-context/`
223
223
 
@@ -246,8 +246,8 @@ templates:
246
246
  ## Guardrails (read → act → write)
247
247
  Before you act:
248
248
  1) Read:
249
- - `notes/plan.md`
250
- - `notes/status.md`
249
+ - `../notes/plan.md`
250
+ - `../notes/status.md`
251
251
  - relevant ticket(s) in `work/in-progress/`
252
252
  - any relevant shared context under `shared-context/`
253
253
 
@@ -276,8 +276,8 @@ templates:
276
276
  ## Guardrails (read → act → write)
277
277
  Before you act:
278
278
  1) Read:
279
- - `notes/plan.md`
280
- - `notes/status.md`
279
+ - `../notes/plan.md`
280
+ - `../notes/status.md`
281
281
  - relevant ticket(s) in `work/in-progress/`
282
282
  - any relevant shared context under `shared-context/`
283
283