@lifeaitools/rdc-skills 0.37.8 → 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/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
|
}
|
|
@@ -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.
|
package/skills/open/SKILL.md
CHANGED
|
@@ -56,9 +56,14 @@ That is a **scoped grant**, not a readout: `permittedActions` and
|
|
|
56
56
|
to "which checkout do I work in" — it already knows pooled repos want a lane.
|
|
57
57
|
|
|
58
58
|
**Unresolved is a result, not an error.** `open regen-root` returns
|
|
59
|
-
`weDoNotHold: ["product_class"]`; `open fsm` returns `unknown_slug`.
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
`weDoNotHold: ["product_class"]`; `open fsm` returns `unknown_slug`. An
|
|
60
|
+
`unknown_slug` — nothing registered at all — means `rdc:new` next: it
|
|
61
|
+
scaffolds and registers a product of the declared class, then hands back to
|
|
62
|
+
this skill to confirm. A `weDoNotHold` entry on an otherwise-known slug is
|
|
63
|
+
narrower — `rdc-harness new <slug> --environment ...` (also wrapped by
|
|
64
|
+
`rdc:new`) fills in just the missing piece. Either way: register the target,
|
|
65
|
+
then open it again — never guess a default and never fall back to hand-rolled
|
|
66
|
+
git.
|
|
62
67
|
|
|
63
68
|
**Two targets at once is two calls.** Each returns its own `bindingId` and its
|
|
64
69
|
own `sourceBoundary`; nothing is exclusive. Working `clauth` and `rdc-harness`
|
|
@@ -156,14 +161,46 @@ Downstream verbs re-resolve from the registry by slug.
|
|
|
156
161
|
2. **Target.** Read `productClass`, `workModel`, `delivery` and `weDoNotHold`
|
|
157
162
|
from the same result. `monorepo_path` NULL means a standalone repo, not in
|
|
158
163
|
this tree. Do not re-query the registry for facts the handle already carries.
|
|
159
|
-
3. **Harness.** Name the shape the target's class implies
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
3. **Harness.** Name the shape the target's class implies. The routes come
|
|
165
|
+
from the RPMS kind registry (`rpms_kinds` → `rpms_usages`); this table is
|
|
166
|
+
**generated from `rpms_kinds`** and never hand-edited. A new kind is one
|
|
167
|
+
row via `register_rpms_kind`, then (in rdc-skills)
|
|
168
|
+
`node scripts/gen-rpms-kind-tables.mjs --from <dump.json>`. Full guide to
|
|
169
|
+
every level, not just product classes: `docs/architecture/KINDS.md` in
|
|
170
|
+
regen-root.
|
|
171
|
+
|
|
172
|
+
<!-- BEGIN GENERATED: rpms-kinds -->
|
|
173
|
+
_Generated from `rpms_kinds` + `rpms_usages` (schema `rpms.kinds.v1`) by `scripts/gen-rpms-kind-tables.mjs` — do not hand-edit. Product-level kinds only; every other level is in `select rpms_kinds_snapshot()`._
|
|
174
|
+
|
|
175
|
+
| kind | home | build · dev (`ship_route`) | runtime | prod (`deploy_route`) |
|
|
176
|
+
|---|---|---|---|---|
|
|
177
|
+
| `app` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
|
|
178
|
+
| `artifact` | `artifacts/<slug>/` | `artifact-export` | — | — (no-production-target) |
|
|
179
|
+
| `bpmn-process` | `docs/systems/<system>/bpmn/` | `flowable-dev-release` | `java` | `flowable-release` |
|
|
180
|
+
| `city-tool` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
|
|
181
|
+
| `corpus` | `$CORPUS_ROOT/<CO>/docs/` | `content-write` | — | — (content-not-code) |
|
|
182
|
+
| `dmn-decision` _(proposed)_ | `docs/systems/<system>/dmn/` | `dmn-test-cases` | `java` | `flowable-dmn-release` |
|
|
183
|
+
| `documentation` | `docs/<slug>/` | `artifact-export` | — | — (no-production-target) |
|
|
184
|
+
| `dsl` _(proposed)_ | `packages/<pkg>/src/dsl/` | `grammar-suite` | — | — (production-is-registry) |
|
|
185
|
+
| `education-piece` | `artifacts/<slug>/` | `artifact-export` | — | — (no-production-target) |
|
|
186
|
+
| `grammar` | `packages/<pkg>/src/grammar/` | `grammar-suite` | — | — (production-is-registry) |
|
|
187
|
+
| `mcp` | `mcp-servers/<slug>/` | `plugin-registration` | `ts-server` | `declared-service-route` |
|
|
188
|
+
| `model` | `models/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
|
|
189
|
+
| `ontology` | `packages/planetary-ontology/src/` | `grammar-suite` | — | — (production-is-registry) |
|
|
190
|
+
| `package` | `packages/<slug>/` | `registry-release` | — | — (production-is-registry) |
|
|
191
|
+
| `prototype` | `artifacts/<slug>/` | — | — | — (no-production-target) |
|
|
192
|
+
| `share-app` | `apps/<slug>/` | `pm2-development` | `next` | `declared-promotion` |
|
|
193
|
+
| `site-html` | `sites/<slug>/` | `static-development` | `static-html` | `declared-promotion` |
|
|
194
|
+
| `site-ts` | `sites/<slug>/` | `static-development` | `static-html` | `declared-promotion` |
|
|
195
|
+
| `skill` | `skills/<slug>/` | `local-install` | — | `release-tag` |
|
|
196
|
+
| `theme` | — | `content-write` | — | — (content-not-code) |
|
|
197
|
+
| `vendor` | `vendor/<slug>/` | `registry-release` | — | — (production-is-registry) |
|
|
198
|
+
| `worker` | `workers/<slug>/` | `edge-dev` | — | `edge-deploy` |
|
|
199
|
+
<!-- END GENERATED -->
|
|
200
|
+
|
|
201
|
+
The concrete build command is the target's own (`pnpm --filter <pkg>
|
|
202
|
+
build` in a monorepo, its own tooling standalone); the route names what
|
|
203
|
+
kind of ship and deploy it owes.
|
|
167
204
|
|
|
168
205
|
4. **Visual-test contract (for an app/site with UI work).** Read the target
|
|
169
206
|
`package.json` and name its `test:ui` command and Playwright config. A
|
|
@@ -216,6 +253,8 @@ Downstream verbs re-resolve from the registry by slug.
|
|
|
216
253
|
|
|
217
254
|
## Related
|
|
218
255
|
|
|
256
|
+
- `rdc:new` — an `unknown_slug` or a `weDoNotHold` gap routes here: scaffold and
|
|
257
|
+
register a product, or just its deployment environment, then re-open.
|
|
219
258
|
- `guides/work-contract.md` — the contract this skill opens: rows, tiers, proving,
|
|
220
259
|
staleness, and what Stop checks.
|
|
221
260
|
- `rdc:flow` — declares what KIND of work this is. `rdc-work start --type` sets it
|