@lifeaitools/rdc-skills 0.37.7 → 0.37.9
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 +73 -1
- package/README.md +1 -0
- package/clauth-plugin.json +1 -1
- package/commands/flow.md +16 -15
- package/commands/mode.md +6 -0
- package/data/rpms-kinds.snapshot.json +2485 -0
- package/git-sha.json +1 -1
- package/package.json +2 -2
- package/scripts/gen-rpms-kind-tables.mjs +210 -0
- package/scripts/lib/plugins/static-call-reference.mjs +109 -0
- package/scripts/lib/plugins/treesitter.mjs +13 -3
- package/skills/co-develop/SKILL.md +5 -0
- package/skills/collab/COLLABORATOR.md +9 -1
- package/skills/collab/SKILL.md +25 -1
- package/skills/new/SKILL.md +154 -0
- package/skills/open/SKILL.md +50 -11
- package/skills/regen-media/SKILL.md +122 -17
- package/tests/lib/jsx-references.test.mjs +113 -0
package/git-sha.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/rdc-skills",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.9",
|
|
4
4
|
"description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"type": "plugin",
|
|
20
20
|
"skills": "skills/",
|
|
21
21
|
"guides": "guides/",
|
|
22
|
-
"version": "0.37.
|
|
22
|
+
"version": "0.37.9",
|
|
23
23
|
"commands": "commands/"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the kind tables in skills/new/SKILL.md and skills/open/SKILL.md
|
|
4
|
+
* from ONE source: the RPMS kind registry.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS
|
|
7
|
+
*
|
|
8
|
+
* `public.rpms_kinds` + `public.rpms_usages` (Supabase uvojezuorjgqzmhhgluu)
|
|
9
|
+
* are the single home for every kind of development: its level, usage, home,
|
|
10
|
+
* ship route and deploy route. Both skills used to carry a hand-written copy of
|
|
11
|
+
* that list ("the eleven values --product-type accepts", and the class ->
|
|
12
|
+
* build/dev/prod table). Two hand copies of one fact drift; a projection does
|
|
13
|
+
* not. A new kind is one row via `register_rpms_kind` — then refresh the
|
|
14
|
+
* snapshot and re-run this script.
|
|
15
|
+
*
|
|
16
|
+
* SOURCE
|
|
17
|
+
*
|
|
18
|
+
* data/rpms-kinds.snapshot.json — a committed, byte-stable copy of
|
|
19
|
+
* `select rpms_kinds_snapshot()` (schema "rpms.kinds.v1"). Keys are sorted
|
|
20
|
+
* recursively and `kinds` / `usages` are sorted by slug, so a refresh that
|
|
21
|
+
* changes nothing produces no diff.
|
|
22
|
+
*
|
|
23
|
+
* TARGETS
|
|
24
|
+
*
|
|
25
|
+
* Each target file holds a delimited block:
|
|
26
|
+
* <!-- BEGIN GENERATED: rpms-kinds -->
|
|
27
|
+
* ...
|
|
28
|
+
* <!-- END GENERATED -->
|
|
29
|
+
* Everything between the markers is rewritten; the prose around it is not.
|
|
30
|
+
* The block inherits the indentation of its BEGIN marker.
|
|
31
|
+
*
|
|
32
|
+
* Run:
|
|
33
|
+
* node scripts/gen-rpms-kind-tables.mjs rewrite both blocks
|
|
34
|
+
* node scripts/gen-rpms-kind-tables.mjs --check exit 1 if a block is stale
|
|
35
|
+
* node scripts/gen-rpms-kind-tables.mjs --from <raw.json>
|
|
36
|
+
* canonicalise a raw `rpms_kinds_snapshot()` dump into the snapshot file,
|
|
37
|
+
* then rewrite both blocks
|
|
38
|
+
*/
|
|
39
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
|
|
43
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
44
|
+
const SNAPSHOT = path.join(ROOT, 'data', 'rpms-kinds.snapshot.json');
|
|
45
|
+
const SCHEMA = 'rpms.kinds.v1';
|
|
46
|
+
const BEGIN = '<!-- BEGIN GENERATED: rpms-kinds -->';
|
|
47
|
+
const END = '<!-- END GENERATED -->';
|
|
48
|
+
const PRODUCT_TYPE_DECL = 'rdc-harness:PRODUCT_CLASSES';
|
|
49
|
+
|
|
50
|
+
/** Recursively sort object keys so serialisation is byte-stable. */
|
|
51
|
+
export function canonicalise(value) {
|
|
52
|
+
if (Array.isArray(value)) return value.map(canonicalise);
|
|
53
|
+
if (value && typeof value === 'object') {
|
|
54
|
+
return Object.fromEntries(
|
|
55
|
+
Object.keys(value).sort().map((k) => [k, canonicalise(value[k])]),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bySlug = (a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
62
|
+
|
|
63
|
+
/** Validate and canonicalise a raw snapshot object. */
|
|
64
|
+
export function normaliseSnapshot(raw) {
|
|
65
|
+
const snap = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
66
|
+
if (snap?.schema !== SCHEMA) throw new Error(`snapshot schema is ${snap?.schema}, expected ${SCHEMA}`);
|
|
67
|
+
if (!Array.isArray(snap.kinds) || !Array.isArray(snap.usages)) throw new Error('snapshot needs kinds[] and usages[]');
|
|
68
|
+
return canonicalise({
|
|
69
|
+
...snap,
|
|
70
|
+
kinds: [...snap.kinds].sort(bySlug),
|
|
71
|
+
usages: [...snap.usages].sort(bySlug),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function serialiseSnapshot(snap) {
|
|
76
|
+
return `${JSON.stringify(snap, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const code = (s) => `\`${String(s).replace(/\|/g, '\\|')}\``;
|
|
80
|
+
const dash = '—';
|
|
81
|
+
|
|
82
|
+
function usageMap(snap) {
|
|
83
|
+
return new Map(snap.usages.map((u) => [u.slug, u]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function productKinds(snap) {
|
|
87
|
+
return snap.kinds.filter((k) => k.level === 'product');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const acceptsProductType = (k) => (k.declared_in || []).includes(PRODUCT_TYPE_DECL);
|
|
91
|
+
|
|
92
|
+
function statusNote(k) {
|
|
93
|
+
return k.status === 'active' ? '' : ` _(${k.status})_`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function prodCell(u) {
|
|
97
|
+
if (!u) return dash;
|
|
98
|
+
if (u.deploy_route) return code(u.deploy_route);
|
|
99
|
+
return u.inapplicable_reason ? `${dash} (${u.inapplicable_reason})` : dash;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** skills/new: the product-level kind table. */
|
|
103
|
+
export function renderNewTable(snap) {
|
|
104
|
+
const usages = usageMap(snap);
|
|
105
|
+
const kinds = productKinds(snap);
|
|
106
|
+
const accepted = kinds.filter(acceptsProductType).length;
|
|
107
|
+
const lines = [
|
|
108
|
+
`_${kinds.length} product-level kinds; ${accepted} accepted by \`--product-type\` today. Generated from \`rpms_kinds\` (schema \`${snap.schema}\`) by \`scripts/gen-rpms-kind-tables.mjs\` — do not hand-edit._`,
|
|
109
|
+
'',
|
|
110
|
+
'| kind | level | usage | ship | deploy | home | `--product-type` today |',
|
|
111
|
+
'|---|---|---|---|---|---|---|',
|
|
112
|
+
];
|
|
113
|
+
for (const k of kinds) {
|
|
114
|
+
const u = usages.get(k.usage);
|
|
115
|
+
lines.push([
|
|
116
|
+
'',
|
|
117
|
+
`${code(k.slug)}${statusNote(k)}`,
|
|
118
|
+
k.level,
|
|
119
|
+
code(k.usage),
|
|
120
|
+
u?.ship_route ? code(u.ship_route) : dash,
|
|
121
|
+
prodCell(u),
|
|
122
|
+
k.home_template ? code(k.home_template) : dash,
|
|
123
|
+
acceptsProductType(k) ? 'yes' : dash,
|
|
124
|
+
'',
|
|
125
|
+
].join(' | ').trim());
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** skills/open: kind -> build/dev/prod routes. */
|
|
131
|
+
export function renderOpenTable(snap) {
|
|
132
|
+
const usages = usageMap(snap);
|
|
133
|
+
const lines = [
|
|
134
|
+
`_Generated from \`rpms_kinds\` + \`rpms_usages\` (schema \`${snap.schema}\`) by \`scripts/gen-rpms-kind-tables.mjs\` — do not hand-edit. Product-level kinds only; every other level is in \`select rpms_kinds_snapshot()\`._`,
|
|
135
|
+
'',
|
|
136
|
+
'| kind | home | build · dev (`ship_route`) | runtime | prod (`deploy_route`) |',
|
|
137
|
+
'|---|---|---|---|---|',
|
|
138
|
+
];
|
|
139
|
+
for (const k of productKinds(snap)) {
|
|
140
|
+
const u = usages.get(k.usage);
|
|
141
|
+
lines.push([
|
|
142
|
+
'',
|
|
143
|
+
`${code(k.slug)}${statusNote(k)}`,
|
|
144
|
+
k.home_template ? code(k.home_template) : dash,
|
|
145
|
+
u?.ship_route ? code(u.ship_route) : dash,
|
|
146
|
+
u?.runtime ? code(u.runtime) : dash,
|
|
147
|
+
prodCell(u),
|
|
148
|
+
'',
|
|
149
|
+
].join(' | ').trim());
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Replace the delimited block in `text` with `lines`, keeping the marker indent. */
|
|
155
|
+
export function replaceBlock(text, lines, file) {
|
|
156
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
157
|
+
const src = text.split(/\r?\n/);
|
|
158
|
+
const b = src.findIndex((l) => l.trim() === BEGIN);
|
|
159
|
+
const e = src.findIndex((l, i) => i > b && l.trim() === END);
|
|
160
|
+
if (b < 0 || e < 0) throw new Error(`${file}: missing ${BEGIN} … ${END} markers`);
|
|
161
|
+
const indent = src[b].match(/^\s*/)[0];
|
|
162
|
+
const body = lines.map((l) => (l ? indent + l : ''));
|
|
163
|
+
return [...src.slice(0, b + 1), ...body, ...src.slice(e)].join(eol);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const TARGETS = [
|
|
167
|
+
{ file: path.join('skills', 'new', 'SKILL.md'), render: renderNewTable },
|
|
168
|
+
{ file: path.join('skills', 'open', 'SKILL.md'), render: renderOpenTable },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
function main(argv) {
|
|
172
|
+
const check = argv.includes('--check');
|
|
173
|
+
const fromIdx = argv.indexOf('--from');
|
|
174
|
+
if (fromIdx >= 0) {
|
|
175
|
+
if (check) throw new Error('--from and --check are exclusive');
|
|
176
|
+
const raw = readFileSync(path.resolve(argv[fromIdx + 1]), 'utf8');
|
|
177
|
+
writeFileSync(SNAPSHOT, serialiseSnapshot(normaliseSnapshot(raw)));
|
|
178
|
+
console.log(`snapshot written: ${path.relative(ROOT, SNAPSHOT)}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const onDisk = readFileSync(SNAPSHOT, 'utf8');
|
|
182
|
+
const snap = normaliseSnapshot(onDisk);
|
|
183
|
+
const stale = [];
|
|
184
|
+
if (serialiseSnapshot(snap) !== onDisk) stale.push(`${path.relative(ROOT, SNAPSHOT)} (not canonical)`);
|
|
185
|
+
|
|
186
|
+
for (const t of TARGETS) {
|
|
187
|
+
const abs = path.join(ROOT, t.file);
|
|
188
|
+
const before = readFileSync(abs, 'utf8');
|
|
189
|
+
const after = replaceBlock(before, t.render(snap), t.file);
|
|
190
|
+
if (after === before) continue;
|
|
191
|
+
stale.push(t.file);
|
|
192
|
+
if (!check) writeFileSync(abs, after);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (check && stale.length) {
|
|
196
|
+
console.error(`rpms kind tables out of date (${stale.length}): ${stale.join(', ')} — run: node scripts/gen-rpms-kind-tables.mjs`);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
console.log(check ? 'rpms kind tables current' : `rpms kind tables: ${stale.length} file(s) rewritten`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
204
|
+
try {
|
|
205
|
+
process.exit(main(process.argv.slice(2)));
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error(`gen-rpms-kind-tables: ${err.message}`);
|
|
208
|
+
process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Resolve known POSSIBLE call targets, not observed runtime calls. A dynamic
|
|
2
|
+
// key into a closed literal registry can select any of its entries, just as
|
|
3
|
+
// either branch of an if can call its target. Open/escaped registries stay
|
|
4
|
+
// unresolved. These private parser facts do not change the public unit schema.
|
|
5
|
+
function unwrap(node) {
|
|
6
|
+
while (node && ['as_expression', 'satisfies_expression', 'parenthesized_expression', 'non_null_expression'].includes(node.type)) node = node.namedChild(0);
|
|
7
|
+
return node;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function children(node) { return node?.namedChildren ?? []; }
|
|
11
|
+
|
|
12
|
+
function containsName(node, name) {
|
|
13
|
+
return node?.text === name || children(node).some(child => containsName(child, name));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasAssignment(node, name) {
|
|
17
|
+
const target = ['assignment_expression', 'augmented_assignment_expression', 'for_in_statement'].includes(node.type)
|
|
18
|
+
? node.childForFieldName('left') : node.type === 'update_expression' ? node.childForFieldName('argument') : null;
|
|
19
|
+
return containsName(target, name) || children(node).some(child => hasAssignment(child, name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function binding(name, site) {
|
|
23
|
+
for (let scope = site.parent; scope; scope = scope.parent) {
|
|
24
|
+
if (['function_expression', 'class', 'class_expression'].includes(scope.type) && scope.childForFieldName('name')?.text === name) return { blocked: true };
|
|
25
|
+
const parameters = scope.childForFieldName('parameters') ?? scope.childForFieldName('parameter');
|
|
26
|
+
if (parameters && new RegExp(`\\b${name.replace(/[$]/g, '\\$&')}\\b`).test(parameters.text)) return { blocked: true };
|
|
27
|
+
if (scope.type === 'for_in_statement' && containsName(scope.childForFieldName('left'), name)) return { blocked: true };
|
|
28
|
+
if (scope.type === 'for_statement' && containsName(scope.childForFieldName('initializer'), name)) return { blocked: true };
|
|
29
|
+
if (!['statement_block', 'program'].includes(scope.type)) continue;
|
|
30
|
+
for (const statement of children(scope)) {
|
|
31
|
+
const declaration = statement.type === 'export_statement' ? statement.childForFieldName('declaration') : statement;
|
|
32
|
+
if (declaration?.childForFieldName('name')?.text === name) {
|
|
33
|
+
return { blocked: scope.type !== 'program' || hasAssignment(scope, name), terminal: true, node: declaration.childForFieldName('name') };
|
|
34
|
+
}
|
|
35
|
+
for (const entry of children(declaration)) {
|
|
36
|
+
if (entry.type !== 'variable_declarator' || !containsName(entry.childForFieldName('name'), name)) continue;
|
|
37
|
+
return { node: entry.childForFieldName('value'), declaration: entry, scope,
|
|
38
|
+
blocked: entry.childForFieldName('name')?.type !== 'identifier' || declaration.children[0]?.text !== 'const' || entry.startIndex > site.startIndex };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasUnsafeUse(found, name) {
|
|
46
|
+
let unsafe = false;
|
|
47
|
+
function visit(node) {
|
|
48
|
+
if (['identifier', 'shorthand_property_identifier'].includes(node.type) && node.text === name && node.startIndex !== found.declaration.childForFieldName('name').startIndex) {
|
|
49
|
+
const parent = node.parent;
|
|
50
|
+
const isSelection = ['member_expression', 'subscript_expression'].includes(parent?.type) && parent.childForFieldName('object')?.id === node.id;
|
|
51
|
+
let selected = isSelection ? parent : node;
|
|
52
|
+
while (['parenthesized_expression', 'as_expression', 'non_null_expression', 'array_pattern', 'object_pattern', 'pair_pattern'].includes(selected.parent?.type)) selected = selected.parent;
|
|
53
|
+
const container = selected.parent;
|
|
54
|
+
const writesSelection = ['assignment_expression', 'augmented_assignment_expression', 'update_expression', 'unary_expression'].includes(container?.type)
|
|
55
|
+
|| (container?.type === 'for_in_statement' && container.childForFieldName('left')?.id === selected.id);
|
|
56
|
+
// Passing or aliasing the object can expose it to mutation. No whole-program guess.
|
|
57
|
+
if (!isSelection || writesSelection) unsafe = true;
|
|
58
|
+
}
|
|
59
|
+
for (const child of children(node)) visit(child);
|
|
60
|
+
}
|
|
61
|
+
visit(found.scope);
|
|
62
|
+
return unsafe;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function propertyName(node) {
|
|
66
|
+
if (!node) return null;
|
|
67
|
+
if (['property_identifier', 'identifier', 'shorthand_property_identifier'].includes(node.type)) return node.text;
|
|
68
|
+
if (node.type === 'string') return node.namedChildren.every(child => child.type === 'string_fragment') ? node.text.slice(1, -1) : null;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveValue(node, seen) {
|
|
73
|
+
node = unwrap(node);
|
|
74
|
+
if (!node || seen.has(node.id)) return [];
|
|
75
|
+
seen = new Set([...seen, node.id]);
|
|
76
|
+
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier') {
|
|
77
|
+
const found = binding(node.text, node);
|
|
78
|
+
if (!found) return [node];
|
|
79
|
+
if (found.blocked) return [];
|
|
80
|
+
if (found.terminal) return [found.node];
|
|
81
|
+
return resolveValue(found.node, seen);
|
|
82
|
+
}
|
|
83
|
+
if (node.type === 'binary_expression' && node.childForFieldName('operator')?.text === '??') {
|
|
84
|
+
return [...resolveValue(node.childForFieldName('left'), seen), ...resolveValue(node.childForFieldName('right'), seen)];
|
|
85
|
+
}
|
|
86
|
+
if (!['member_expression', 'subscript_expression'].includes(node.type)) return [];
|
|
87
|
+
const object = node.childForFieldName('object');
|
|
88
|
+
const keyNode = node.childForFieldName(node.type === 'member_expression' ? 'property' : 'index');
|
|
89
|
+
// A runtime key gives a set of known possible targets, never a definite one.
|
|
90
|
+
const key = node.type === 'subscript_expression' && keyNode?.type !== 'string' ? null : propertyName(keyNode);
|
|
91
|
+
if (object?.type !== 'identifier') return [];
|
|
92
|
+
const found = binding(object.text, node);
|
|
93
|
+
if (!found || found.blocked || found.terminal || hasUnsafeUse(found, object.text)) return [];
|
|
94
|
+
// An exported object can be mutated by another module; const seals only its binding.
|
|
95
|
+
if (found.declaration.parent?.parent?.type === 'export_statement') return [];
|
|
96
|
+
const registry = unwrap(found.node);
|
|
97
|
+
if (registry?.type !== 'object' || registry.namedChildren.some(entry => !['pair', 'shorthand_property_identifier'].includes(entry.type))) return [];
|
|
98
|
+
const keys = registry.namedChildren.map(entry => propertyName(entry.childForFieldName('key') ?? entry));
|
|
99
|
+
if (keys.includes(null) || keys.includes('__proto__') || new Set(keys).size !== keys.length) return [];
|
|
100
|
+
const entries = registry.namedChildren.filter((entry, index) => key === null || keys[index] === key);
|
|
101
|
+
return entries.flatMap(entry => resolveValue(entry.childForFieldName('value') ?? entry, seen));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Private parser helper: known possible callees; [] means unresolved, not impossible. */
|
|
105
|
+
export function staticCallReference(node) {
|
|
106
|
+
return [...new Set(resolveValue(node, new Set())
|
|
107
|
+
.filter(value => ['identifier', 'shorthand_property_identifier'].includes(value?.type))
|
|
108
|
+
.map(value => value.text))];
|
|
109
|
+
}
|
|
@@ -141,6 +141,7 @@ import { extractMembers } from '../vendor/codeflow-parser/memberFacts.js';
|
|
|
141
141
|
import { createRequire } from 'node:module';
|
|
142
142
|
import { readFileSync } from 'node:fs';
|
|
143
143
|
import { basename } from 'node:path';
|
|
144
|
+
import { staticCallReference } from './static-call-reference.mjs';
|
|
144
145
|
|
|
145
146
|
const require = createRequire(import.meta.url);
|
|
146
147
|
|
|
@@ -423,9 +424,18 @@ function fieldsOf(bodyNode) {
|
|
|
423
424
|
function callsOf(bodyNode) {
|
|
424
425
|
const out = [];
|
|
425
426
|
walkSelfAndDescendants(bodyNode, (n) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
427
|
+
const jsx = n.type === 'jsx_opening_element' || n.type === 'jsx_self_closing_element';
|
|
428
|
+
if (n.type !== 'call_expression' && !jsx) return;
|
|
429
|
+
const fn = n.childForFieldName(jsx ? 'name' : 'function');
|
|
430
|
+
if (!fn || (jsx && fn.type === 'identifier' && /^[a-z]/.test(fn.text))) return;
|
|
431
|
+
const resolved = staticCallReference(fn);
|
|
432
|
+
if (jsx) {
|
|
433
|
+
if (resolved.length) out.push(...resolved);
|
|
434
|
+
else if (fn.type === 'member_expression') out.push(fn.text.replace(/^this\./, ''));
|
|
435
|
+
} else {
|
|
436
|
+
out.push(fn.text.replace(/^this\./, ''));
|
|
437
|
+
out.push(...resolved.filter(target => target !== fn.text));
|
|
438
|
+
}
|
|
429
439
|
});
|
|
430
440
|
return out;
|
|
431
441
|
}
|
|
@@ -8,6 +8,11 @@ description: rdc:co-develop (subcommand) - [--wait] — peer-aware Claude/Codex
|
|
|
8
8
|
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
9
9
|
> **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Under the flag, do not create/send/reply to live co-development turns; report the intended clauth command and mark the external relay action as skipped.
|
|
10
10
|
|
|
11
|
+
> **⚠️ Transport gone (probed 2026-09-24).** `clauth codevelop …` returns `unknown command 'codevelop'`
|
|
12
|
+
> and `GET /codevelop` returns `Unknown endpoint` on clauth 2.16.8. The commands below cannot run.
|
|
13
|
+
> Use `rdc:collab` (named lane via `pool/collab-lane.mjs`, ask via `pool/collab-ask.mjs`, reply file
|
|
14
|
+
> under `.rdc/reports/`). This skill is kept only until it is retired or re-pointed.
|
|
15
|
+
|
|
11
16
|
# rdc:co-develop — Peer-Aware Co-Development
|
|
12
17
|
|
|
13
18
|
Use this skill when an RDC workflow needs Claude and Codex to work together in
|
|
@@ -67,6 +67,14 @@ that guard just made impossible. That happened on 2026-08-14 and cost a complete
|
|
|
67
67
|
negotiation result. **Your reasoning in the reply is always deliverable. A file
|
|
68
68
|
write may not be.**
|
|
69
69
|
|
|
70
|
+
**One exception — a named report.** If the dispatch carries `DELIVERABLE: <path>` and
|
|
71
|
+
the path is under `.rdc/` (a report in `.rdc/reports/`, a plan in
|
|
72
|
+
`.rdc/areas/<domain>/<program>/plans/`, scratch in `.rdc/tmp/`), write exactly that file and no
|
|
73
|
+
other: the start gate exempts `.rdc/**` docs, so it cannot strand, and the reply still carries the
|
|
74
|
+
summary. If the path is anywhere else, do **not** write it: return the content in the reply and
|
|
75
|
+
say `BLOCKED: DELIVERABLE outside .rdc/ would be refused by work-contract-required`. Never create
|
|
76
|
+
a work contract to get past it; that is the initiator's decision.
|
|
77
|
+
|
|
70
78
|
---
|
|
71
79
|
|
|
72
80
|
## What you owe BACK — your responsibilities
|
|
@@ -282,7 +290,7 @@ cannot act on a guess it has to verify.
|
|
|
282
290
|
| Filing a settled agreement as a lesson | Buries a queryable Decision |
|
|
283
291
|
| Going silent when blocked | Indistinguishable from being dead |
|
|
284
292
|
| Editing a document you do not own | Lost work, no error, no trace |
|
|
285
|
-
| Writing ANY file during a collab | Not your job here — the reply is the deliverable |
|
|
293
|
+
| Writing ANY file during a collab | Not your job here — the reply is the deliverable. Only exception: the one `DELIVERABLE:` path under `.rdc/` named in the dispatch |
|
|
286
294
|
| Committing to whatever branch you happen to be on | The change strands where nobody looks for it |
|
|
287
295
|
| Answering an easier adjacent question | The real point stays open and looks settled |
|
|
288
296
|
| Attempting a workaround for a guard block | The guard is usually right; the dispatch is usually wrong |
|
package/skills/collab/SKILL.md
CHANGED
|
@@ -224,7 +224,7 @@ most common failure and it fails at the guard layer, not the prompt layer.
|
|
|
224
224
|
|
|
225
225
|
| Collaborator | Invocation | Isolation requirement | Known failure mode |
|
|
226
226
|
|---|---|---|---|
|
|
227
|
-
| **`codex`** | `codex exec [-C <dir>] "<msg>"` · resume: `codex exec resume <session-id> "<msg>"` | **A headless `codex exec` peer
|
|
227
|
+
| **`codex`** | `codex exec [-C <dir>] "<msg>"` · resume: `codex exec resume <session-id> "<msg>"` | **A headless `codex exec` peer can write ONE named report under `.rdc/` (probed 2026-09-24); any other write in a git work tree is refused by `work-contract-required` unless it holds a contract — see the notes below.** Never dispatch it from your own lane, and launch it with its own lane's identity: `env -u CELL_ROLE LIFEAI_CHECKOUT_ROOT=<its lane> codex exec -C <its lane> -m <model> "<prompt>" < /dev/null` (a headless peer inherits your `CELL_ROLE`/`LIFEAI_CHECKOUT_ROOT` and then fails lane recovery with "identity … differs"; probed 2026-09-24: with this env it starts clean). Close stdin with `< /dev/null`: an open pipe makes `codex exec` wait forever. | Its managed identity is fixed at session creation and `-C` does **not** change it. Dispatch from your lane and every write is refused with `CODEX MANAGED LANE: App Local must use an owned managed Codex lane` — permanently, for that session's whole life. |
|
|
228
228
|
|
|
229
229
|
> **`-C` does not establish lane identity — verified 2026-08-14, and an earlier
|
|
230
230
|
> version of this table said the opposite.** Managed identity is an 8-field record
|
|
@@ -235,6 +235,14 @@ most common failure and it fails at the guard layer, not the prompt layer.
|
|
|
235
235
|
> `codex exec` has no lane-claim path, so it cannot produce that record — which
|
|
236
236
|
> means **a headless Codex peer cannot write in any lane, including its own**.
|
|
237
237
|
>
|
|
238
|
+
> **Correction, probed 2026-09-24.** `codex exec -s workspace-write` wrote a file in a lane
|
|
239
|
+
> it did not own with no identity refusal, so "cannot write in any lane" above is too strong. What
|
|
240
|
+
> stops a peer is the START gate, `work-contract-required` (guard-rules.mjs): a write inside a git
|
|
241
|
+
> work tree needs a work contract, and **only `.rdc/**` files with a doc extension (`md`, `txt`,
|
|
242
|
+
> `json`, `yml`, `csv`, `html`, …) are exempt.** A peer told to write `REVIEW-REPLY.md` at a lane
|
|
243
|
+
> root was refused (2026-09-24) and had to hand the finished report back through its log. The flow
|
|
244
|
+
> does not change this: the rule never reads it. Name the reply path under `.rdc/` (rule 7, Step 2).
|
|
245
|
+
>
|
|
238
246
|
> Two consequences: (1) `resume` replays the session's *recorded* identity, so a
|
|
239
247
|
> session created in the wrong cwd is poisoned for life and `-C` will not repair
|
|
240
248
|
> it — start a new session instead; (2) treat a `codex exec` peer as a
|
|
@@ -391,6 +399,21 @@ Rules that make the contract hold:
|
|
|
391
399
|
OUT of the collab still lands in a governing document (Step 5); it is the
|
|
392
400
|
negotiation turns themselves that are conversation-only.
|
|
393
401
|
|
|
402
|
+
7. **A report or plan the peer must hand back goes to ONE named path under `.rdc/`,
|
|
403
|
+
stated in the dispatch.** Add the line `DELIVERABLE: <path>` (or
|
|
404
|
+
`DELIVERABLE: reply-only`) to the contract. Reports: `.rdc/reports/<lane>-<topic>.md`.
|
|
405
|
+
Plans: `.rdc/areas/<domain>/<program>/plans/<name>.md`. Scratch: `.rdc/tmp/`. These are the
|
|
406
|
+
only places a peer without a work contract can write, so the file cannot strand. A path at a
|
|
407
|
+
lane root or in any code or docs directory will be refused, and the peer's answer then
|
|
408
|
+
exists only in its transcript. The peer still returns a short summary in the reply; the file
|
|
409
|
+
is the durable copy. Name the peer the single writer of that one file (Step 4).
|
|
410
|
+
|
|
411
|
+
**Cap the file in the dispatch, or it will not be short.** Peers asked for "the literal result" of
|
|
412
|
+
every probe paste raw dumps (2026-09-24: a 12-finding audit came back as 99 KB). State: findings
|
|
413
|
+
at most 4 lines each, `EVIDENCE` one command plus a one-line result, raw output to
|
|
414
|
+
`.rdc/tmp/<lane>-evidence/` and linked, whole file at most 15 KB, a time box, and "append after each
|
|
415
|
+
point" so a partial report still exists.
|
|
416
|
+
|
|
394
417
|
---
|
|
395
418
|
|
|
396
419
|
## Step 3 — Converge
|
|
@@ -571,6 +594,7 @@ and avoids acting on a misread premise (lesson
|
|
|
571
594
|
| Escalating a transcript | The human has to find the question themselves |
|
|
572
595
|
| Two agents editing one document | Lost work, no error |
|
|
573
596
|
| Writing to the repo during a collab | Your answer may be guard-blocked and stranded; the reply is always deliverable, a write may not be |
|
|
597
|
+
| Naming a reply file outside `.rdc/` (a lane root, `docs/`, a package) | Refused by `work-contract-required`; the finished report has to be dug out of the peer's log. Use `DELIVERABLE: .rdc/reports/<name>.md` |
|
|
574
598
|
| Using collab to hand off WORK | Wrong skill. Create a work item and use rdc:build or an isolated-worktree agent |
|
|
575
599
|
|
|
576
600
|
---
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: new
|
|
3
|
+
description: rdc:new <slug> --product-type <t> [--environment <e> --host-type <t> --url <u>] — scaffold and register a new product, then optionally register its deployment environment
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
|
|
7
|
+
> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
|
|
8
|
+
> One checklist upfront, updated in place, shown again at end with a 1-line verdict.
|
|
9
|
+
|
|
10
|
+
# rdc:new — create and register a product, no register-then-improvise gap
|
|
11
|
+
|
|
12
|
+
## The gap this closes
|
|
13
|
+
|
|
14
|
+
`/rdc:open` is gate-only: it resolves an **already-registered** slug and
|
|
15
|
+
refuses to guess a default for one that isn't. Nothing named what to do next.
|
|
16
|
+
The real capability existed one layer down, unwrapped: `rdc-harness create`
|
|
17
|
+
(scaffold + register) and `rdc-harness new` (register a deployment
|
|
18
|
+
environment). This skill is the guided flow over both — pick a product class,
|
|
19
|
+
scaffold it, register the environment(s) it needs, then hand you straight to
|
|
20
|
+
`rdc:open` to confirm.
|
|
21
|
+
|
|
22
|
+
## Run the script. Do not re-derive this by hand.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness create <slug> --product-type <t> --monorepo-root <dir>
|
|
26
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness new <slug> --environment <e> --host-type <t> --url <u>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Same package as `rdc:open` — `@lifeai/rdc-harness` on GitHub Packages, needs
|
|
30
|
+
`@lifeai:registry=https://npm.pkg.github.com` in `.npmrc` (full setup:
|
|
31
|
+
`.rdc/guides/agent-bootstrap.md`). `new` needs `@lifeai/rdc-harness@>=0.5.0`
|
|
32
|
+
— the environment-registration verb; anything older 404s the flag.
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
### 1. Pick the kind
|
|
37
|
+
|
|
38
|
+
Every kind of development lives in ONE place: the RPMS kind registry
|
|
39
|
+
(`public.rpms_kinds` + `public.rpms_usages`; read it with
|
|
40
|
+
`select rpms_kinds_snapshot()`). The table below is **generated from
|
|
41
|
+
`rpms_kinds`** — its level, usage, ship and deploy routes and home all come
|
|
42
|
+
from the registry, never from this file. A new kind is **one row via
|
|
43
|
+
`register_rpms_kind`**; then refresh the snapshot and regenerate
|
|
44
|
+
(rdc-skills: `node scripts/gen-rpms-kind-tables.mjs --from <dump.json>`). Never invent a
|
|
45
|
+
kind here, and never hand-edit the block.
|
|
46
|
+
|
|
47
|
+
**`--kind <slug>` is the forward name for `--product-type`.** For product-level
|
|
48
|
+
kinds the values are the same slugs. `--product-type` remains the flag the
|
|
49
|
+
current `rdc-harness create` CLI takes — keep passing `--product-type <t>` to
|
|
50
|
+
it, and only for a kind marked **yes** in the last column (the harness enum,
|
|
51
|
+
byte-identical to `repo_registry`'s product-class reference). A product-level
|
|
52
|
+
kind without **yes** is registered in `rpms_kinds` but has no `create`
|
|
53
|
+
scaffold yet: stop and say so rather than picking a near neighbour.
|
|
54
|
+
|
|
55
|
+
Full guide to every kind at every level (not just product-level), and the path to add a new one:
|
|
56
|
+
`docs/architecture/KINDS.md` in regen-root.
|
|
57
|
+
|
|
58
|
+
<!-- BEGIN GENERATED: rpms-kinds -->
|
|
59
|
+
_22 product-level kinds; 11 accepted by `--product-type` today. Generated from `rpms_kinds` (schema `rpms.kinds.v1`) by `scripts/gen-rpms-kind-tables.mjs` — do not hand-edit._
|
|
60
|
+
|
|
61
|
+
| kind | level | usage | ship | deploy | home | `--product-type` today |
|
|
62
|
+
|---|---|---|---|---|---|---|
|
|
63
|
+
| `app` | product | `public-surface` | `pm2-development` | `declared-promotion` | `apps/<slug>/` | yes |
|
|
64
|
+
| `artifact` | product | `export` | `artifact-export` | — (no-production-target) | `artifacts/<slug>/` | yes |
|
|
65
|
+
| `bpmn-process` | product | `process` | `flowable-dev-release` | `flowable-release` | `docs/systems/<system>/bpmn/` | — |
|
|
66
|
+
| `city-tool` | product | `public-surface` | `pm2-development` | `declared-promotion` | `apps/<slug>/` | — |
|
|
67
|
+
| `corpus` | product | `content` | `content-write` | — (content-not-code) | `$CORPUS_ROOT/<CO>/docs/` | yes |
|
|
68
|
+
| `dmn-decision` _(proposed)_ | product | `decision` | `dmn-test-cases` | `flowable-dmn-release` | `docs/systems/<system>/dmn/` | — |
|
|
69
|
+
| `documentation` | product | `export` | `artifact-export` | — (no-production-target) | `docs/<slug>/` | yes |
|
|
70
|
+
| `dsl` _(proposed)_ | product | `language` | `grammar-suite` | — (production-is-registry) | `packages/<pkg>/src/dsl/` | — |
|
|
71
|
+
| `education-piece` | product | `export` | `artifact-export` | — (no-production-target) | `artifacts/<slug>/` | — |
|
|
72
|
+
| `grammar` | product | `language` | `grammar-suite` | — (production-is-registry) | `packages/<pkg>/src/grammar/` | — |
|
|
73
|
+
| `mcp` | product | `agent-tool` | `plugin-registration` | `declared-service-route` | `mcp-servers/<slug>/` | yes |
|
|
74
|
+
| `model` | product | `public-surface` | `pm2-development` | `declared-promotion` | `models/<slug>/` | yes |
|
|
75
|
+
| `ontology` | product | `language` | `grammar-suite` | — (production-is-registry) | `packages/planetary-ontology/src/` | — |
|
|
76
|
+
| `package` | product | `library` | `registry-release` | — (production-is-registry) | `packages/<slug>/` | yes |
|
|
77
|
+
| `prototype` | product | `exploration` | — | — (no-production-target) | `artifacts/<slug>/` | yes |
|
|
78
|
+
| `share-app` | product | `embed` | `pm2-development` | `declared-promotion` | `apps/<slug>/` | yes |
|
|
79
|
+
| `site-html` | product | `static-site` | `static-development` | `declared-promotion` | `sites/<slug>/` | yes |
|
|
80
|
+
| `site-ts` | product | `static-site` | `static-development` | `declared-promotion` | `sites/<slug>/` | yes |
|
|
81
|
+
| `skill` | product | `agent-instruction` | `local-install` | `release-tag` | `skills/<slug>/` | — |
|
|
82
|
+
| `theme` | product | `content` | `content-write` | — (content-not-code) | — | — |
|
|
83
|
+
| `vendor` | product | `library` | `registry-release` | — (production-is-registry) | `vendor/<slug>/` | — |
|
|
84
|
+
| `worker` | product | `edge` | `edge-dev` | `edge-deploy` | `workers/<slug>/` | — |
|
|
85
|
+
<!-- END GENERATED -->
|
|
86
|
+
|
|
87
|
+
If the ask is a VLAS reference model (`model`), stop here and use `rdc:new-model`
|
|
88
|
+
instead — it registers all four VLAS-required homes; this skill's `create` verb scaffolds files; it does not touch the
|
|
89
|
+
four fixed homes a model needs.
|
|
90
|
+
|
|
91
|
+
### 2. Scaffold and register
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness create <slug> --product-type <t> --monorepo-root <dir>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Registry-first-then-files: writes `repo_registry` (or the app's row, for a
|
|
98
|
+
monorepo target), then scaffolds. A failure after the registry write is a
|
|
99
|
+
**registered-but-empty** target — `rdc:open <slug>` will resolve it with
|
|
100
|
+
`weDoNotHold` naming what's missing; re-run `create`, do not hand-write the
|
|
101
|
+
registry row to route around it.
|
|
102
|
+
|
|
103
|
+
### 3. Register the environment(s) it needs
|
|
104
|
+
|
|
105
|
+
**Dev-vs-prod-and-shared-docker is a class-wide POLICY, not a question this
|
|
106
|
+
step asks you.** `.claude/rules/infrastructure-contract.md` Rule 12 already
|
|
107
|
+
decides it: a public-facing app or site runs on Docker in both dev and main;
|
|
108
|
+
an MCP or daemon stays on PM2 everywhere. Apply that rule, then register
|
|
109
|
+
accordingly — don't prompt the person for a choice the policy already makes:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness new <slug> --environment dev --host-type <t> --url <dev-url>
|
|
113
|
+
# only if the class is public-facing (app/site) and the work is ready for it:
|
|
114
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness new <slug> --environment prod --host-type <t> --url <prod-url>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`--host-type` names the real runtime (`coolify`, `pm2`, …), not a guess. A
|
|
118
|
+
Coolify target additionally needs `--coolify-uuid <uuid> --coolify-server
|
|
119
|
+
<ip>` — resolve these via `/knowledge`/clauth, never hardcode.
|
|
120
|
+
|
|
121
|
+
A `dev`-only registration is the normal, complete outcome for most new work —
|
|
122
|
+
**do not register `prod` speculatively.** Prod promotion is a separate,
|
|
123
|
+
later, deliberate act (`/rdc:release`, `/rdc:deploy … promote`).
|
|
124
|
+
|
|
125
|
+
### 4. Confirm with `rdc:open`
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx -y --package=@lifeai/rdc-harness rdc-harness open <slug>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Must return `opened: true` with no `weDoNotHold` entries for the fields this
|
|
132
|
+
skill just set (`product_class`, and `deployment_record` once step 3 ran).
|
|
133
|
+
Any remaining `weDoNotHold` entry names a real gap — do not paper over it by
|
|
134
|
+
re-running `create`/`new` with guessed values.
|
|
135
|
+
|
|
136
|
+
## Checklist
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
[ ] kind chosen from the generated rpms_kinds table (--product-type = a "yes" kind), or routed to rdc:new-model
|
|
140
|
+
[ ] rdc-harness create — scaffold + registry write succeeded
|
|
141
|
+
[ ] environment(s) registered per infrastructure-contract Rule 12, not by asking
|
|
142
|
+
[ ] prod NOT registered speculatively (dev-only unless promotion is the actual ask)
|
|
143
|
+
[ ] rdc:open <slug> confirms — opened:true, no unexpected weDoNotHold
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Related
|
|
147
|
+
|
|
148
|
+
- `rdc:open` — the gate this hands off to; resolves what `new` just registered.
|
|
149
|
+
- `rdc:new-model` — VLAS Reference Models only; registers all four required
|
|
150
|
+
homes in one gated flow. Use it instead of this skill for a model.
|
|
151
|
+
- `.claude/rules/infrastructure-contract.md` — Rule 12 (Docker vs PM2 by
|
|
152
|
+
class), and the deployment registry contract `new` writes into.
|
|
153
|
+
- `rdc:deploy` / `rdc:release` — consume the registration this skill creates;
|
|
154
|
+
prod promotion is their job, not this skill's.
|