@lifeaitools/rdc-skills 0.9.31 → 0.9.33

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.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +24 -2
  2. package/.github/workflows/self-test.yml +34 -34
  3. package/MANIFEST.md +4 -0
  4. package/commands/build.md +183 -183
  5. package/commands/collab.md +180 -180
  6. package/commands/deploy.md +152 -138
  7. package/commands/fixit.md +116 -104
  8. package/commands/handoff.md +173 -173
  9. package/commands/overnight.md +220 -220
  10. package/commands/plan.md +158 -158
  11. package/commands/preplan.md +131 -131
  12. package/commands/prototype.md +145 -145
  13. package/commands/report.md +99 -99
  14. package/commands/review.md +120 -120
  15. package/commands/status.md +86 -86
  16. package/commands/workitems.md +132 -127
  17. package/guides/agent-bootstrap.md +265 -206
  18. package/guides/agents/backend.md +104 -104
  19. package/guides/agents/content.md +94 -94
  20. package/guides/agents/cs2.md +56 -56
  21. package/guides/agents/data.md +87 -87
  22. package/guides/agents/design.md +77 -77
  23. package/guides/agents/frontend.md +92 -92
  24. package/guides/agents/infrastructure.md +81 -81
  25. package/guides/agents/setup.md +279 -279
  26. package/guides/agents/verify.md +119 -119
  27. package/guides/agents/viz.md +106 -106
  28. package/hooks/foreground-process-gate.js +109 -0
  29. package/hooks/hook-logger.js +25 -0
  30. package/hooks/run-hidden-hook.ps1 +47 -0
  31. package/hooks/work-item-exit-gate.js +297 -0
  32. package/package.json +3 -3
  33. package/rules/work-items-rpc.md +56 -7
  34. package/scripts/fixtures/guides/bad-guide.md +15 -0
  35. package/scripts/fixtures/guides-clean/good-guide.md +16 -0
  36. package/scripts/install-rdc-skills.js +53 -9
  37. package/scripts/install.ps1 +17 -11
  38. package/scripts/self-test.mjs +1323 -1113
  39. package/scripts/test-guide-validator.mjs +194 -0
  40. package/skills/build/SKILL.md +355 -355
  41. package/skills/co-develop/SKILL.md +182 -0
  42. package/skills/collab/SKILL.md +217 -217
  43. package/skills/deploy/SKILL.md +198 -152
  44. package/skills/design/SKILL.md +211 -211
  45. package/skills/fixit/SKILL.md +132 -122
  46. package/skills/handoff/SKILL.md +200 -200
  47. package/skills/help/SKILL.md +104 -102
  48. package/skills/overnight/SKILL.md +224 -224
  49. package/skills/plan/SKILL.md +252 -251
  50. package/skills/preplan/SKILL.md +86 -86
  51. package/skills/prototype/SKILL.md +150 -150
  52. package/skills/release/SKILL.md +342 -342
  53. package/skills/report/SKILL.md +100 -100
  54. package/skills/review/SKILL.md +121 -120
  55. package/skills/self-test/SKILL.md +126 -126
  56. package/skills/status/SKILL.md +99 -97
  57. package/skills/terminal-config/SKILL.md +18 -18
  58. package/skills/watch/SKILL.md +91 -91
  59. package/skills/workitems/SKILL.md +151 -146
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse hook — hard-stop unsafe work item exit transitions.
4
+ *
5
+ * This gate runs before tool calls and blocks legacy or supervisor-mutated
6
+ * CodeFlow/work-item close patterns before they reach Supabase. The database
7
+ * remains the final authority; this hook gives agents an immediate stop sign.
8
+ */
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const hookLog = require('./hook-logger');
15
+
16
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
17
+ const DEFAULT_SUPABASE_URL = 'https://uvojezuorjgqzmhhgluu.supabase.co';
18
+ const EVENT_LOG = path.join(os.homedir(), '.claude', 'work-item-checklist-events.jsonl');
19
+
20
+ function readStdin() {
21
+ return new Promise((resolve) => {
22
+ let input = '';
23
+ process.stdin.setEncoding('utf8');
24
+ process.stdin.on('data', (chunk) => { input += chunk; });
25
+ process.stdin.on('end', () => resolve(input));
26
+ process.stdin.resume();
27
+ });
28
+ }
29
+
30
+ function block(message, details = {}) {
31
+ hookLog('work-item-exit-gate', 'PreToolUse', 'block', details);
32
+ process.stdout.write(JSON.stringify({
33
+ systemMessage: `HARD BLOCK — Work item exit gate rejected this tool call.\n\n${message}`,
34
+ }));
35
+ process.exit(1);
36
+ }
37
+
38
+ function pass(details = {}) {
39
+ hookLog('work-item-exit-gate', 'PreToolUse', 'pass', details);
40
+ process.exit(0);
41
+ }
42
+
43
+ function stringifyTool(toolInput) {
44
+ if (typeof toolInput === 'string') return toolInput;
45
+ try { return JSON.stringify(toolInput); } catch { return ''; }
46
+ }
47
+
48
+ function stripCasts(value) {
49
+ return String(value || '')
50
+ .replace(/::\s*[a-z_][\w.]*/gi, '')
51
+ .trim();
52
+ }
53
+
54
+ function unquote(value) {
55
+ const v = stripCasts(value);
56
+ const m = v.match(/^'(.*)'$/s) || v.match(/^"(.*)"$/s);
57
+ return (m ? m[1] : v).replace(/''/g, "'").trim();
58
+ }
59
+
60
+ function getNamedArg(text, name) {
61
+ const re = new RegExp(`${name}\\s*:?=\\s*('(?:''|[^'])*'|"(?:\\\\"|[^"])*"|true|false|null|[0-9a-f-]{36})`, 'i');
62
+ const m = text.match(re);
63
+ return m ? unquote(m[1]) : null;
64
+ }
65
+
66
+ function splitArgs(argsText) {
67
+ const args = [];
68
+ let current = '';
69
+ let quote = null;
70
+ let depth = 0;
71
+ for (let i = 0; i < argsText.length; i++) {
72
+ const ch = argsText[i];
73
+ const next = argsText[i + 1];
74
+ if (quote) {
75
+ current += ch;
76
+ if (ch === quote && !(quote === "'" && next === "'")) quote = null;
77
+ else if (ch === "'" && next === "'") current += argsText[++i];
78
+ continue;
79
+ }
80
+ if (ch === "'" || ch === '"') {
81
+ quote = ch;
82
+ current += ch;
83
+ continue;
84
+ }
85
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
86
+ if (ch === ')' || ch === ']' || ch === '}') depth = Math.max(0, depth - 1);
87
+ if (ch === ',' && depth === 0) {
88
+ args.push(current.trim());
89
+ current = '';
90
+ continue;
91
+ }
92
+ current += ch;
93
+ }
94
+ if (current.trim()) args.push(current.trim());
95
+ return args;
96
+ }
97
+
98
+ function findCall(text, name) {
99
+ const idx = text.toLowerCase().indexOf(name.toLowerCase());
100
+ if (idx < 0) return null;
101
+ const open = text.indexOf('(', idx + name.length);
102
+ if (open < 0) return null;
103
+ let quote = null;
104
+ let depth = 0;
105
+ for (let i = open; i < text.length; i++) {
106
+ const ch = text[i];
107
+ const next = text[i + 1];
108
+ if (quote) {
109
+ if (ch === quote && !(quote === "'" && next === "'")) quote = null;
110
+ else if (ch === "'" && next === "'") i++;
111
+ continue;
112
+ }
113
+ if (ch === "'" || ch === '"') {
114
+ quote = ch;
115
+ continue;
116
+ }
117
+ if (ch === '(') depth++;
118
+ if (ch === ')') {
119
+ depth--;
120
+ if (depth === 0) return text.slice(open + 1, i);
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ function objectArg(blob, name) {
127
+ const re = new RegExp(`"${name}"\\s*:\\s*("([^"]*)"|true|false|null)`, 'i');
128
+ const m = blob.match(re);
129
+ if (!m) return null;
130
+ if (m[1] === 'true' || m[1] === 'false' || m[1] === 'null') return m[1];
131
+ return m[2] || null;
132
+ }
133
+
134
+ function extractStatusCall(blob) {
135
+ if (!/update_work_item_status/i.test(blob)) return null;
136
+ const call = findCall(blob, 'update_work_item_status');
137
+ const args = call ? splitArgs(call) : [];
138
+ return {
139
+ id: objectArg(blob, 'p_id') || getNamedArg(blob, 'p_id') || unquote(args[0] || '').match(UUID_RE)?.[0] || blob.match(UUID_RE)?.[0] || null,
140
+ status: objectArg(blob, 'p_status') || getNamedArg(blob, 'p_status') || unquote(args[1] || ''),
141
+ actorSessionId: objectArg(blob, 'p_actor_session_id') || getNamedArg(blob, 'p_actor_session_id') || unquote(args[3] || ''),
142
+ actorRole: objectArg(blob, 'p_actor_role') || getNamedArg(blob, 'p_actor_role') || unquote(args[4] || ''),
143
+ };
144
+ }
145
+
146
+ function extractTickCall(blob) {
147
+ if (!/update_checklist_item/i.test(blob)) return null;
148
+ const call = findCall(blob, 'update_checklist_item');
149
+ const args = call ? splitArgs(call) : [];
150
+ return {
151
+ workItemId: objectArg(blob, 'p_work_item_id') || getNamedArg(blob, 'p_work_item_id') || unquote(args[0] || '').match(UUID_RE)?.[0] || blob.match(UUID_RE)?.[0] || null,
152
+ itemId: objectArg(blob, 'p_item_id') || getNamedArg(blob, 'p_item_id') || unquote(args[1] || ''),
153
+ checked: (objectArg(blob, 'p_checked') || getNamedArg(blob, 'p_checked') || unquote(args[2] || '')).toLowerCase() === 'true',
154
+ actorSessionId: objectArg(blob, 'p_actor_session_id') || getNamedArg(blob, 'p_actor_session_id') || unquote(args[3] || ''),
155
+ actorRole: objectArg(blob, 'p_actor_role') || getNamedArg(blob, 'p_actor_role') || unquote(args[4] || ''),
156
+ };
157
+ }
158
+
159
+ function logTick(tick, rawTool) {
160
+ try {
161
+ fs.mkdirSync(path.dirname(EVENT_LOG), { recursive: true });
162
+ fs.appendFileSync(EVENT_LOG, JSON.stringify({
163
+ ts: new Date().toISOString(),
164
+ work_item_id: tick.workItemId || null,
165
+ item_id: tick.itemId || null,
166
+ checked: tick.checked,
167
+ actor_session_id: tick.actorSessionId || null,
168
+ actor_role: tick.actorRole || null,
169
+ tool_name: rawTool.tool_name || null,
170
+ }) + '\n');
171
+ } catch (_) {}
172
+ }
173
+
174
+ async function getServiceKey() {
175
+ if (process.env.SUPABASE_SERVICE_ROLE_KEY) return process.env.SUPABASE_SERVICE_ROLE_KEY;
176
+ if (process.env.SUPABASE_SERVICE_KEY) return process.env.SUPABASE_SERVICE_KEY;
177
+ for (const endpoint of ['supabase-service', 'supabase-service-role']) {
178
+ try {
179
+ const res = await fetch(`http://127.0.0.1:52437/v/${endpoint}`, { signal: AbortSignal.timeout(2000) });
180
+ if (res.ok) {
181
+ const text = (await res.text()).trim();
182
+ if (text && !text.startsWith('{')) return text;
183
+ }
184
+ } catch (_) {}
185
+ }
186
+ return null;
187
+ }
188
+
189
+ async function supabaseGet(pathname) {
190
+ const key = await getServiceKey();
191
+ if (!key) throw new Error('clauth/env did not provide a Supabase service key');
192
+ const base = (process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL).replace(/\/$/, '');
193
+ const res = await fetch(`${base}/rest/v1/${pathname}`, {
194
+ headers: {
195
+ apikey: key,
196
+ Authorization: `Bearer ${key}`,
197
+ Accept: 'application/json',
198
+ },
199
+ signal: AbortSignal.timeout(3500),
200
+ });
201
+ if (!res.ok) throw new Error(`Supabase verification HTTP ${res.status}`);
202
+ return res.json();
203
+ }
204
+
205
+ function requiredItems(checklist) {
206
+ return Array.isArray(checklist)
207
+ ? checklist.filter((item) => item && item.required === true)
208
+ : [];
209
+ }
210
+
211
+ async function verifyDone(statusCall, blob) {
212
+ if (!statusCall.id) block('`update_work_item_status(..., done)` must include a work item UUID.', statusCall);
213
+ if (!statusCall.actorSessionId || !statusCall.actorRole) {
214
+ block('`done` requires the 5-argument RPC shape with `p_actor_session_id` and `p_actor_role`.', statusCall);
215
+ }
216
+ if (statusCall.actorRole !== 'validator') {
217
+ block('Only a validator may transition a non-epic work item to `done`; implementation agents move work to `review`.', statusCall);
218
+ }
219
+ if (/submit_implementation_report/i.test(blob)) {
220
+ block('Submit the implementation report in a separate tool call before `done`; the gate verifies committed DB state.', statusCall);
221
+ }
222
+
223
+ let rows;
224
+ let events;
225
+ try {
226
+ rows = await supabaseGet(`work_items?id=eq.${encodeURIComponent(statusCall.id)}&select=id,status,item_type,implementation_report,checklist`);
227
+ events = await supabaseGet(`work_item_checklist_events?work_item_id=eq.${encodeURIComponent(statusCall.id)}&select=item_id,checked,actor_session_id,actor_role,created_at&order=created_at.desc&limit=200`);
228
+ } catch (e) {
229
+ block(`Cannot live-verify work item exit gate: ${e.message}. Do not close the item until clauth/Supabase verification is available.`, statusCall);
230
+ }
231
+
232
+ const item = rows && rows[0];
233
+ if (!item) block(`Work item ${statusCall.id} was not found.`, statusCall);
234
+ if (item.item_type === 'epic') return;
235
+ if (item.status !== 'review') block('Non-epic work items must be in `review` before a validator may mark them `done`.', { ...statusCall, currentStatus: item.status });
236
+
237
+ const report = item.implementation_report;
238
+ if (!report || typeof report !== 'object') block('`done` rejected because `implementation_report` is null or not an object.', statusCall);
239
+ if (!report.codeflow_post || typeof report.codeflow_post !== 'object') block('`done` rejected because `implementation_report.codeflow_post` is missing.', statusCall);
240
+
241
+ const missing = requiredItems(item.checklist).filter((ci) => ci.checked !== true);
242
+ if (missing.length > 0) {
243
+ block(`Required checklist items are unchecked: ${missing.map((ci) => ci.id || ci.text || '<unknown>').join(', ')}`, statusCall);
244
+ }
245
+
246
+ const latestByItem = new Map();
247
+ for (const event of Array.isArray(events) ? events : []) {
248
+ if (!latestByItem.has(event.item_id)) latestByItem.set(event.item_id, event);
249
+ }
250
+ const supervisorReticks = requiredItems(item.checklist)
251
+ .map((ci) => latestByItem.get(ci.id))
252
+ .filter((event) => event && event.checked === true && event.actor_role !== 'agent');
253
+ if (supervisorReticks.length > 0) {
254
+ block(
255
+ `Required checklist items were last ticked by a non-agent session: ${supervisorReticks.map((e) => e.item_id).join(', ')}`,
256
+ { ...statusCall, supervisorReticks: supervisorReticks.map((e) => e.item_id) },
257
+ );
258
+ }
259
+ }
260
+
261
+ function validateTick(tick, rawTool) {
262
+ logTick(tick, rawTool);
263
+ if (!tick.checked) return;
264
+ if (!tick.workItemId || !tick.itemId) {
265
+ block('Checklist ticks must include `p_work_item_id` and `p_item_id`.', tick);
266
+ }
267
+ if (!tick.actorSessionId) {
268
+ block('Checklist ticks must include `p_actor_session_id`; unaudited ticks are rejected.', tick);
269
+ }
270
+ if (tick.actorRole !== 'agent') {
271
+ block('Only the originating implementation agent may tick required checklist evidence. Supervisors/validators reopen or review; they do not re-tick.', tick);
272
+ }
273
+ }
274
+
275
+ async function main() {
276
+ let raw;
277
+ try { raw = JSON.parse(await readStdin()); } catch { process.exit(0); }
278
+
279
+ const blob = stringifyTool(raw.tool_input || raw);
280
+ const tick = extractTickCall(blob);
281
+ if (tick) validateTick(tick, raw);
282
+
283
+ const statusCall = extractStatusCall(blob);
284
+ if (!statusCall) pass({ reason: 'no-status-call' });
285
+
286
+ const status = String(statusCall.status || '').toLowerCase();
287
+ if (status === 'review' && (!statusCall.actorSessionId || statusCall.actorRole !== 'agent')) {
288
+ block('Implementation agents must move completed work to `review` with `p_actor_session_id` and `p_actor_role := agent`.', statusCall);
289
+ }
290
+ if (status === 'done') {
291
+ await verifyDone(statusCall, blob);
292
+ }
293
+
294
+ pass({ status });
295
+ }
296
+
297
+ main().catch((e) => block(`Exit gate crashed: ${e.message}`));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.31",
4
- "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight builds",
3
+ "version": "0.9.33",
4
+ "description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
7
7
  "claude-code-plugin",
@@ -19,7 +19,7 @@
19
19
  "type": "plugin",
20
20
  "skills": "skills/",
21
21
  "guides": "guides/",
22
- "version": "0.9.26",
22
+ "version": "0.9.33",
23
23
  "commands": "commands/"
24
24
  },
25
25
  "scripts": {
@@ -135,6 +135,10 @@ const { data } = await supabase.rpc('insert_work_item', {
135
135
 
136
136
  Transition a work item's status. Auto-manages `completed_at`.
137
137
 
138
+ **Hard gate:** implementation agents do not set non-epic work directly to `done`.
139
+ They submit a report and move the item to `review`. A validator closes it as
140
+ `done` only after fresh verification.
141
+
138
142
  **Supabase SQL:**
139
143
  ```sql
140
144
  SELECT update_work_item_status(
@@ -142,10 +146,20 @@ SELECT update_work_item_status(
142
146
  'in_progress'
143
147
  );
144
148
 
149
+ SELECT update_work_item_status(
150
+ 'work-item-uuid'::uuid,
151
+ 'review',
152
+ '["Implementation complete; ready for validator"]'::jsonb,
153
+ 'agent-session-id',
154
+ 'agent'
155
+ );
156
+
145
157
  SELECT update_work_item_status(
146
158
  'work-item-uuid'::uuid,
147
159
  'done',
148
- '["Completed by agent session xyz"]'::jsonb
160
+ '["Validator verified implementation report, CodeFlow post, and checklist evidence"]'::jsonb,
161
+ 'validator-session-id',
162
+ 'validator'
149
163
  );
150
164
  ```
151
165
 
@@ -156,10 +170,20 @@ const { data } = await supabase.rpc('update_work_item_status', {
156
170
  p_status: 'in_progress'
157
171
  });
158
172
 
173
+ const { data } = await supabase.rpc('update_work_item_status', {
174
+ p_id: itemId,
175
+ p_status: 'review',
176
+ p_notes_append: ['Implementation complete; ready for validator'],
177
+ p_actor_session_id: agentSessionId,
178
+ p_actor_role: 'agent'
179
+ });
180
+
159
181
  const { data } = await supabase.rpc('update_work_item_status', {
160
182
  p_id: itemId,
161
183
  p_status: 'done',
162
- p_notes_append: ['Completed by agent session xyz']
184
+ p_notes_append: ['Validator verified implementation report, CodeFlow post, and checklist evidence'],
185
+ p_actor_session_id: validatorSessionId,
186
+ p_actor_role: 'validator'
163
187
  });
164
188
  ```
165
189
 
@@ -170,10 +194,13 @@ const { data } = await supabase.rpc('update_work_item_status', {
170
194
  | `p_id` | uuid | yes | work_item ID |
171
195
  | `p_status` | text | yes | One of: `todo`, `in_progress`, `blocked`, `review`, `done`, `archived` |
172
196
  | `p_notes_append` | jsonb | no | Array of strings appended to existing notes |
197
+ | `p_actor_session_id` | text | review/done | Current agent or validator session ID |
198
+ | `p_actor_role` | text | review/done | `agent` for `review`, `validator` for `done` |
173
199
 
174
200
  **Behavior:**
175
201
  - Setting `done` auto-sets `completed_at = now()`
176
- - Setting `done` raises EXCEPTION if any `required: true` checklist item has `checked: false` — DoD gate
202
+ - Setting non-epic work to `review` raises EXCEPTION unless `implementation_report.codeflow_post` exists and caller role is `agent`
203
+ - Setting non-epic work to `done` raises EXCEPTION unless current status is `review`, caller role is `validator`, `implementation_report.codeflow_post` exists, every required checklist item is checked, and required checklist evidence was ticked by the originating agent session
177
204
  - Setting `todo`, `in_progress`, `blocked`, or `review` clears `completed_at`
178
205
  - `updated_at` always refreshed to current timestamp
179
206
  - Raises exception if ID not found
@@ -190,7 +217,10 @@ const { data } = await supabase.rpc('update_work_item_status', {
190
217
  SELECT update_checklist_item(
191
218
  'work-item-uuid'::uuid,
192
219
  'tsc-clean', -- string id of the checklist item
193
- true -- checked state
220
+ true, -- checked state
221
+ 'agent-session-id',
222
+ 'agent',
223
+ 'rpc'
194
224
  );
195
225
  ```
196
226
 
@@ -201,8 +231,13 @@ SELECT update_checklist_item(
201
231
  | `p_work_item_id` | uuid | yes | the work item ID |
202
232
  | `p_item_id` | text | yes | the `id` field of the checklist item |
203
233
  | `p_checked` | boolean | yes | `true` = checked, `false` = unchecked |
234
+ | `p_actor_session_id` | text | yes for checked=true | originating implementation agent session ID |
235
+ | `p_actor_role` | text | yes for checked=true | must be `agent` for completion evidence ticks |
236
+ | `p_event_source` | text | no | source label, default `rpc` |
204
237
 
205
238
  Call this as each checklist item is completed — not all at once at the end.
239
+ The tick is written to `work_item_checklist_events`; supervisor or validator
240
+ re-ticks are rejected by the exit gate.
206
241
 
207
242
  **Returns:** updated checklist JSONB array.
208
243
 
@@ -210,7 +245,7 @@ Call this as each checklist item is completed — not all at once at the end.
210
245
 
211
246
  ### 5. submit_implementation_report — File agent completion narrative
212
247
 
213
- MUST be called BEFORE `update_work_item_status('done')`.
248
+ MUST be called BEFORE `update_work_item_status(..., 'review', ...)`.
214
249
 
215
250
  **Supabase SQL:**
216
251
  ```sql
@@ -222,7 +257,14 @@ SELECT submit_implementation_report(
222
257
  "deviations": [],
223
258
  "uncertainty": [],
224
259
  "detail": "Full narrative",
225
- "flags": []
260
+ "flags": [],
261
+ "codeflow_post": {
262
+ "agent_session_id": "agent-session-id",
263
+ "summary": "What changed and why",
264
+ "files_changed": ["path/to/file"],
265
+ "verification": ["command or evidence"],
266
+ "commit": "optional commit hash"
267
+ }
226
268
  }'::jsonb
227
269
  );
228
270
  ```
@@ -244,7 +286,14 @@ SELECT submit_implementation_report(
244
286
  "deviations": ["string — deviation from plan", "..."],
245
287
  "uncertainty": ["string — anything unclear", "..."],
246
288
  "detail": "string — full narrative",
247
- "flags": ["string — supervisor attention needed", "..."]
289
+ "flags": ["string — supervisor attention needed", "..."],
290
+ "codeflow_post": {
291
+ "agent_session_id": "string — originating implementation session",
292
+ "summary": "string — agent-authored CodeFlow post",
293
+ "files_changed": ["string", "..."],
294
+ "verification": ["string", "..."],
295
+ "commit": "string — optional"
296
+ }
248
297
  }
249
298
  ```
250
299
 
@@ -0,0 +1,15 @@
1
+ # Bad Infrastructure Guide — FIXTURE (contains banned terms for self-test)
2
+ > This file is used by the rdc:self-test guide-content validator fixture test.
3
+ > It intentionally contains banned terms in positive-instruction context.
4
+
5
+ ## Coolify Access
6
+
7
+ All Coolify operations via `@masonator/coolify-mcp` (38 tools, pre-authenticated).
8
+
9
+ Use the `@masonator/coolify-mcp` MCP tool to deploy apps.
10
+
11
+ You can also use brand-studio for design token work via @regen/brand-studio.
12
+
13
+ ## Clauth Key
14
+
15
+ Use `curl -s http://127.0.0.1:52437/v/nonexistent-key` for that service.
@@ -0,0 +1,16 @@
1
+ # Good Infrastructure Guide — FIXTURE (clean, no banned terms)
2
+ > This file is used by the rdc:self-test guide-content validator fixture test.
3
+ > It is a clean guide with no banned terms.
4
+
5
+ ## Coolify Access
6
+
7
+ There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
8
+ Never use brand-studio — the canonical name is Studio.
9
+ The @regen/brand-studio package does not exist — use @regen/studio.
10
+
11
+ All Coolify operations use the clauth daemon + REST API:
12
+
13
+ ```bash
14
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
15
+ curl -s -H "Authorization: Bearer $_COOLIFY" https://deploy.regendevcorp.com/api/v1/applications
16
+ ```
@@ -77,6 +77,18 @@ function copyDir(src, dst, ext) {
77
77
  return count;
78
78
  }
79
79
 
80
+ function copyHookFiles(src, dst) {
81
+ if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
82
+ fs.mkdirSync(dst, { recursive: true });
83
+ const files = fs.readdirSync(src).filter(f => /\.(?:js|ps1)$/i.test(f));
84
+ let count = 0;
85
+ for (const f of files) {
86
+ const s = path.join(src, f);
87
+ if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
88
+ }
89
+ return count;
90
+ }
91
+
80
92
  function copyDirRecursive(src, dst) {
81
93
  if (!fs.existsSync(src)) return;
82
94
  fs.mkdirSync(dst, { recursive: true });
@@ -214,6 +226,29 @@ function cleanStaleHooks(hooksDstDir) {
214
226
  return removed;
215
227
  }
216
228
 
229
+ // Project-local hookify docs were a temporary workaround for work item exit
230
+ // enforcement. The plugin hook is now authoritative, so installs clean those
231
+ // local shims from known RDC workspaces instead of leaving two gates to drift.
232
+ function cleanProjectHookifyShims(projectRoot) {
233
+ if (!projectRoot) return 0;
234
+ const hookifyDir = path.join(projectRoot, '.claude');
235
+ if (!fs.existsSync(hookifyDir)) return 0;
236
+ const stale = [
237
+ 'hookify.work-item-done-gate-bash.local.md',
238
+ 'hookify.work-item-done-gate-mcp.local.md',
239
+ 'hookify.work-item-review-gate-bash.local.md',
240
+ 'hookify.work-item-review-gate-mcp.local.md',
241
+ ];
242
+ let removed = 0;
243
+ for (const f of stale) {
244
+ const p = path.join(hookifyDir, f);
245
+ if (fs.existsSync(p)) {
246
+ try { fs.unlinkSync(p); removed++; } catch {}
247
+ }
248
+ }
249
+ return removed;
250
+ }
251
+
217
252
  // ── Cache flush helper ────────────────────────────────────────────────────────
218
253
  // Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
219
254
  // `latest/`, which caused the plugin loader to scan and register every rdc:*
@@ -479,7 +514,7 @@ function buildZip(version) {
479
514
  ? 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
480
515
  : 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
481
516
  execSync(
482
- `"${psExe}" -NoProfile -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
517
+ `"${psExe}" -NoProfile -NonInteractive -WindowStyle Hidden -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
483
518
  { stdio: 'pipe' }
484
519
  );
485
520
  fs.rmSync(tmp, { recursive: true, force: true });
@@ -498,7 +533,10 @@ function buildZip(version) {
498
533
  function buildHooksConfig(hooksDir) {
499
534
  const base = hooksDir.replace(/\\/g, '/');
500
535
  const cmd = (file, msg) => {
501
- const entry = { type: 'command', command: `node "${base}/${file}"` };
536
+ const command = process.platform === 'win32'
537
+ ? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${base}/run-hidden-hook.ps1" "${base}/${file}"`
538
+ : `node "${base}/${file}"`;
539
+ const entry = { type: 'command', command };
502
540
  if (msg) entry.statusMessage = msg;
503
541
  return entry;
504
542
  };
@@ -507,9 +545,15 @@ function buildHooksConfig(hooksDir) {
507
545
  cmd('check-cwd.js'),
508
546
  cmd('check-stale-work-items.js', 'Checking for stale work items...'),
509
547
  ]}],
510
- PreToolUse: [{ matcher: 'Bash', hooks: [
511
- cmd('require-work-item-on-commit.js'),
512
- ]}],
548
+ PreToolUse: [
549
+ { hooks: [
550
+ cmd('foreground-process-gate.js', 'Checking foreground process policy...'),
551
+ cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
552
+ ]},
553
+ { matcher: 'Bash', hooks: [
554
+ cmd('require-work-item-on-commit.js'),
555
+ ]},
556
+ ],
513
557
  PostToolUse: [{ hooks: [
514
558
  cmd('check-services.js'),
515
559
  ]}],
@@ -654,6 +698,8 @@ async function main() {
654
698
  {
655
699
  const staleRemoved = cleanStaleHooks(hooksDst);
656
700
  if (staleRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${staleRemoved} orphaned hook file(s)`);
701
+ const shimRemoved = cleanProjectHookifyShims(codexRoot);
702
+ if (shimRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${shimRemoved} project-local work-item hookify shim(s)`);
657
703
  }
658
704
 
659
705
  // 0.5. Legacy cleanup — remove old commands/rdc/ (pre-plugin-system format)
@@ -722,9 +768,7 @@ async function main() {
722
768
  } catch {}
723
769
  try {
724
770
  if (process.platform === 'win32') {
725
- const winLink = linkPath.replace(/\//g, '\\');
726
- const winTarget = target.replace(/\//g, '\\');
727
- execSync(`cmd /c mklink /J "${winLink}" "${winTarget}"`, { stdio: 'pipe' });
771
+ fs.symlinkSync(target, linkPath, 'junction');
728
772
  } else {
729
773
  fs.symlinkSync(target, linkPath, 'dir');
730
774
  }
@@ -741,7 +785,7 @@ async function main() {
741
785
  }
742
786
 
743
787
  // 3. Hook files
744
- const hookCount = copyDir(hooksSrc, hooksDst, '.js');
788
+ const hookCount = copyHookFiles(hooksSrc, hooksDst);
745
789
  ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
746
790
 
747
791
  // 4. Hook wiring