@lifeaitools/rdc-skills 0.9.32 → 0.9.34
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 +25 -2
- package/MANIFEST.md +4 -0
- package/commands/build.md +2 -2
- package/commands/fixit.md +14 -2
- package/commands/workitems.md +6 -1
- package/guides/agent-bootstrap.md +71 -8
- package/guides/agents/backend.md +2 -2
- package/guides/agents/data.md +1 -1
- package/guides/agents/frontend.md +1 -1
- package/guides/agents/setup.md +4 -2
- package/guides/engineering-behavior.md +43 -0
- 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/install-rdc-skills.js +75 -9
- package/scripts/install.ps1 +17 -11
- package/scripts/self-test.mjs +14 -0
- package/skills/build/SKILL.md +13 -9
- package/skills/co-develop/SKILL.md +182 -0
- package/skills/collab/SKILL.md +1 -1
- package/skills/deploy/SKILL.md +1 -1
- package/skills/design/SKILL.md +3 -3
- package/skills/fixit/SKILL.md +19 -5
- package/skills/fs-mcp/SKILL.md +131 -0
- package/skills/handoff/SKILL.md +1 -1
- package/skills/help/SKILL.md +4 -2
- package/skills/overnight/SKILL.md +3 -3
- package/skills/plan/SKILL.md +4 -3
- package/skills/preplan/SKILL.md +1 -1
- package/skills/prototype/SKILL.md +1 -1
- package/skills/release/SKILL.md +2 -2
- package/skills/report/SKILL.md +1 -1
- package/skills/review/SKILL.md +5 -3
- package/skills/self-test/SKILL.md +1 -1
- package/skills/status/SKILL.md +3 -1
- package/skills/watch/SKILL.md +1 -1
- package/skills/workitems/SKILL.md +7 -2
|
@@ -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.34",
|
|
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.34",
|
|
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
|
|
|
@@ -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 });
|
|
@@ -88,6 +100,22 @@ function copyDirRecursive(src, dst) {
|
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
function copyMissingProjectGuides(projectRoot) {
|
|
104
|
+
if (!projectRoot) return 0;
|
|
105
|
+
const src = path.join(repoRoot, 'guides');
|
|
106
|
+
const dst = path.join(projectRoot, '.rdc', 'guides');
|
|
107
|
+
if (!fs.existsSync(src) || !fs.existsSync(dst)) return 0;
|
|
108
|
+
let copied = 0;
|
|
109
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
110
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
111
|
+
const target = path.join(dst, entry.name);
|
|
112
|
+
if (fs.existsSync(target)) continue;
|
|
113
|
+
fs.copyFileSync(path.join(src, entry.name), target);
|
|
114
|
+
copied++;
|
|
115
|
+
}
|
|
116
|
+
return copied;
|
|
117
|
+
}
|
|
118
|
+
|
|
91
119
|
function readJson(p, fallback = {}) {
|
|
92
120
|
if (!fs.existsSync(p)) return fallback;
|
|
93
121
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
|
@@ -214,6 +242,29 @@ function cleanStaleHooks(hooksDstDir) {
|
|
|
214
242
|
return removed;
|
|
215
243
|
}
|
|
216
244
|
|
|
245
|
+
// Project-local hookify docs were a temporary workaround for work item exit
|
|
246
|
+
// enforcement. The plugin hook is now authoritative, so installs clean those
|
|
247
|
+
// local shims from known RDC workspaces instead of leaving two gates to drift.
|
|
248
|
+
function cleanProjectHookifyShims(projectRoot) {
|
|
249
|
+
if (!projectRoot) return 0;
|
|
250
|
+
const hookifyDir = path.join(projectRoot, '.claude');
|
|
251
|
+
if (!fs.existsSync(hookifyDir)) return 0;
|
|
252
|
+
const stale = [
|
|
253
|
+
'hookify.work-item-done-gate-bash.local.md',
|
|
254
|
+
'hookify.work-item-done-gate-mcp.local.md',
|
|
255
|
+
'hookify.work-item-review-gate-bash.local.md',
|
|
256
|
+
'hookify.work-item-review-gate-mcp.local.md',
|
|
257
|
+
];
|
|
258
|
+
let removed = 0;
|
|
259
|
+
for (const f of stale) {
|
|
260
|
+
const p = path.join(hookifyDir, f);
|
|
261
|
+
if (fs.existsSync(p)) {
|
|
262
|
+
try { fs.unlinkSync(p); removed++; } catch {}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return removed;
|
|
266
|
+
}
|
|
267
|
+
|
|
217
268
|
// ── Cache flush helper ────────────────────────────────────────────────────────
|
|
218
269
|
// Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
|
|
219
270
|
// `latest/`, which caused the plugin loader to scan and register every rdc:*
|
|
@@ -479,7 +530,7 @@ function buildZip(version) {
|
|
|
479
530
|
? 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
|
480
531
|
: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
481
532
|
execSync(
|
|
482
|
-
`"${psExe}" -NoProfile -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
|
|
533
|
+
`"${psExe}" -NoProfile -NonInteractive -WindowStyle Hidden -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
|
|
483
534
|
{ stdio: 'pipe' }
|
|
484
535
|
);
|
|
485
536
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -498,7 +549,10 @@ function buildZip(version) {
|
|
|
498
549
|
function buildHooksConfig(hooksDir) {
|
|
499
550
|
const base = hooksDir.replace(/\\/g, '/');
|
|
500
551
|
const cmd = (file, msg) => {
|
|
501
|
-
const
|
|
552
|
+
const command = process.platform === 'win32'
|
|
553
|
+
? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${base}/run-hidden-hook.ps1" "${base}/${file}"`
|
|
554
|
+
: `node "${base}/${file}"`;
|
|
555
|
+
const entry = { type: 'command', command };
|
|
502
556
|
if (msg) entry.statusMessage = msg;
|
|
503
557
|
return entry;
|
|
504
558
|
};
|
|
@@ -507,9 +561,15 @@ function buildHooksConfig(hooksDir) {
|
|
|
507
561
|
cmd('check-cwd.js'),
|
|
508
562
|
cmd('check-stale-work-items.js', 'Checking for stale work items...'),
|
|
509
563
|
]}],
|
|
510
|
-
PreToolUse: [
|
|
511
|
-
|
|
512
|
-
|
|
564
|
+
PreToolUse: [
|
|
565
|
+
{ hooks: [
|
|
566
|
+
cmd('foreground-process-gate.js', 'Checking foreground process policy...'),
|
|
567
|
+
cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
|
|
568
|
+
]},
|
|
569
|
+
{ matcher: 'Bash', hooks: [
|
|
570
|
+
cmd('require-work-item-on-commit.js'),
|
|
571
|
+
]},
|
|
572
|
+
],
|
|
513
573
|
PostToolUse: [{ hooks: [
|
|
514
574
|
cmd('check-services.js'),
|
|
515
575
|
]}],
|
|
@@ -654,6 +714,8 @@ async function main() {
|
|
|
654
714
|
{
|
|
655
715
|
const staleRemoved = cleanStaleHooks(hooksDst);
|
|
656
716
|
if (staleRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${staleRemoved} orphaned hook file(s)`);
|
|
717
|
+
const shimRemoved = cleanProjectHookifyShims(codexRoot);
|
|
718
|
+
if (shimRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${shimRemoved} project-local work-item hookify shim(s)`);
|
|
657
719
|
}
|
|
658
720
|
|
|
659
721
|
// 0.5. Legacy cleanup — remove old commands/rdc/ (pre-plugin-system format)
|
|
@@ -722,9 +784,7 @@ async function main() {
|
|
|
722
784
|
} catch {}
|
|
723
785
|
try {
|
|
724
786
|
if (process.platform === 'win32') {
|
|
725
|
-
|
|
726
|
-
const winTarget = target.replace(/\//g, '\\');
|
|
727
|
-
execSync(`cmd /c mklink /J "${winLink}" "${winTarget}"`, { stdio: 'pipe' });
|
|
787
|
+
fs.symlinkSync(target, linkPath, 'junction');
|
|
728
788
|
} else {
|
|
729
789
|
fs.symlinkSync(target, linkPath, 'dir');
|
|
730
790
|
}
|
|
@@ -736,12 +796,18 @@ async function main() {
|
|
|
736
796
|
} else {
|
|
737
797
|
info('[2.7] Symlinks — no rdc skill links created (skills may already be linked)');
|
|
738
798
|
}
|
|
799
|
+
const projectGuideCount = copyMissingProjectGuides(codexRoot);
|
|
800
|
+
if (projectGuideCount > 0) {
|
|
801
|
+
ok(`[2.8] Guides — ${projectGuideCount} missing guide(s) copied to ${path.join(codexRoot, '.rdc', 'guides')}`);
|
|
802
|
+
} else {
|
|
803
|
+
info('[2.8] Guides — project .rdc/guides already has base guide files or is absent');
|
|
804
|
+
}
|
|
739
805
|
} else {
|
|
740
806
|
info('[2.7] Symlinks — skipped (no codex root found)');
|
|
741
807
|
}
|
|
742
808
|
|
|
743
809
|
// 3. Hook files
|
|
744
|
-
const hookCount =
|
|
810
|
+
const hookCount = copyHookFiles(hooksSrc, hooksDst);
|
|
745
811
|
ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
|
|
746
812
|
|
|
747
813
|
// 4. Hook wiring
|
package/scripts/install.ps1
CHANGED
|
@@ -58,7 +58,7 @@ if (-not (Test-Path $hooksDir)) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
$srcHooks = Join-Path $repoRoot "hooks"
|
|
61
|
-
$hookFiles = Get-ChildItem -Path $srcHooks -
|
|
61
|
+
$hookFiles = Get-ChildItem -Path $srcHooks -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -in @(".js", ".ps1") }
|
|
62
62
|
foreach ($f in $hookFiles) {
|
|
63
63
|
Copy-Item -Path $f.FullName -Destination (Join-Path $hooksDir $f.Name) -Force
|
|
64
64
|
}
|
|
@@ -85,47 +85,53 @@ if ($SkipHooks) {
|
|
|
85
85
|
SessionStart = @(
|
|
86
86
|
[PSCustomObject]@{
|
|
87
87
|
hooks = @(
|
|
88
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
89
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
88
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-cwd.js`"" },
|
|
89
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-stale-work-items.js`""; statusMessage = "Checking for stale work items..." }
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
92
|
)
|
|
93
93
|
PreToolUse = @(
|
|
94
|
+
[PSCustomObject]@{
|
|
95
|
+
hooks = @(
|
|
96
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/foreground-process-gate.js`""; statusMessage = "Checking foreground process policy..." },
|
|
97
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/work-item-exit-gate.js`""; statusMessage = "Checking work item exit gates..." }
|
|
98
|
+
)
|
|
99
|
+
},
|
|
94
100
|
[PSCustomObject]@{
|
|
95
101
|
matcher = "Bash"
|
|
96
102
|
hooks = @(
|
|
97
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
103
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/require-work-item-on-commit.js`"" }
|
|
98
104
|
)
|
|
99
105
|
}
|
|
100
106
|
)
|
|
101
107
|
PostToolUse = @(
|
|
102
108
|
[PSCustomObject]@{
|
|
103
109
|
hooks = @(
|
|
104
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
110
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-services.js`"" }
|
|
105
111
|
)
|
|
106
112
|
}
|
|
107
113
|
)
|
|
108
114
|
PreCompact = @(
|
|
109
115
|
[PSCustomObject]@{
|
|
110
116
|
hooks = @(
|
|
111
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
117
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/precompact-log.js`"" }
|
|
112
118
|
)
|
|
113
119
|
}
|
|
114
120
|
)
|
|
115
121
|
PostCompact = @(
|
|
116
122
|
[PSCustomObject]@{
|
|
117
123
|
hooks = @(
|
|
118
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
119
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
124
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/postcompact-log.js`"" },
|
|
125
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/restart-brief.js`""; statusMessage = "Writing restart brief..." }
|
|
120
126
|
)
|
|
121
127
|
}
|
|
122
128
|
)
|
|
123
129
|
Stop = @(
|
|
124
130
|
[PSCustomObject]@{
|
|
125
131
|
hooks = @(
|
|
126
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
127
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
128
|
-
[PSCustomObject]@{ type = "command"; command = "
|
|
132
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/rate-limit-retry.js`""; statusMessage = "Checking for rate limits..." },
|
|
133
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/post-work-check.js`""; statusMessage = "Checking for undocumented work..." },
|
|
134
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/no-stop-open-epics.js`""; statusMessage = "Checking for open epics..." }
|
|
129
135
|
)
|
|
130
136
|
}
|
|
131
137
|
)
|