@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.
- package/.claude-plugin/plugin.json +24 -2
- package/.github/workflows/self-test.yml +34 -34
- package/MANIFEST.md +4 -0
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -138
- package/commands/fixit.md +116 -104
- package/commands/handoff.md +173 -173
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/status.md +86 -86
- package/commands/workitems.md +132 -127
- package/guides/agent-bootstrap.md +265 -206
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +279 -279
- package/guides/agents/verify.md +119 -119
- package/guides/agents/viz.md +106 -106
- package/hooks/foreground-process-gate.js +109 -0
- package/hooks/hook-logger.js +25 -0
- package/hooks/run-hidden-hook.ps1 +47 -0
- package/hooks/work-item-exit-gate.js +297 -0
- package/package.json +3 -3
- package/rules/work-items-rpc.md +56 -7
- package/scripts/fixtures/guides/bad-guide.md +15 -0
- package/scripts/fixtures/guides-clean/good-guide.md +16 -0
- package/scripts/install-rdc-skills.js +53 -9
- package/scripts/install.ps1 +17 -11
- package/scripts/self-test.mjs +1323 -1113
- package/scripts/test-guide-validator.mjs +194 -0
- package/skills/build/SKILL.md +355 -355
- package/skills/co-develop/SKILL.md +182 -0
- package/skills/collab/SKILL.md +217 -217
- package/skills/deploy/SKILL.md +198 -152
- package/skills/design/SKILL.md +211 -211
- package/skills/fixit/SKILL.md +132 -122
- package/skills/handoff/SKILL.md +200 -200
- package/skills/help/SKILL.md +104 -102
- package/skills/overnight/SKILL.md +224 -224
- package/skills/plan/SKILL.md +252 -251
- package/skills/preplan/SKILL.md +86 -86
- package/skills/prototype/SKILL.md +150 -150
- package/skills/release/SKILL.md +342 -342
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +121 -120
- package/skills/self-test/SKILL.md +126 -126
- package/skills/status/SKILL.md +99 -97
- package/skills/terminal-config/SKILL.md +18 -18
- package/skills/watch/SKILL.md +91 -91
- 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.
|
|
4
|
-
"description": "RDC typed-agent dispatch skill suite for Claude Code
|
|
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.
|
|
22
|
+
"version": "0.9.33",
|
|
23
23
|
"commands": "commands/"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
package/rules/work-items-rpc.md
CHANGED
|
@@ -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
|
-
'["
|
|
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: ['
|
|
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 `
|
|
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
|
|
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('
|
|
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
|
|
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: [
|
|
511
|
-
|
|
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
|
-
|
|
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 =
|
|
788
|
+
const hookCount = copyHookFiles(hooksSrc, hooksDst);
|
|
745
789
|
ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
|
|
746
790
|
|
|
747
791
|
// 4. Hook wiring
|