@lifeaitools/rdc-skills 0.37.8 → 0.37.10

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/git-sha.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "sha": "b71cf17b326bf3b68432d169032107877bfceb58"
2
+ "sha": "36d09ffe1920d0f5906909cf599953d73c71bff9"
3
3
  }
@@ -385,6 +385,19 @@ async function preToolUse(raw) {
385
385
  const command = raw.tool_input?.command || '';
386
386
  if (!command.includes('git commit')) return process.exit(0);
387
387
 
388
+ // Scope guard (2026-10-01, Dave, direct instruction): this gate exists for
389
+ // the RDC/regen-root work-item contract, not for every git repo on the box.
390
+ // Same pattern as no-stop-open-epics.js's PROJECT_SCOPE check — a commit
391
+ // outside the configured scope (default 'regen-root') is none of this
392
+ // hook's business and must not be fail-closed into requiring an RDC work
393
+ // item it has no way to reference.
394
+ const scope = process.env.RDC_PROJECT_SCOPE || 'regen-root';
395
+ const cwd = (raw.cwd || process.cwd()).replace(/\\/g, '/').toLowerCase();
396
+ if (!cwd.includes(scope.toLowerCase())) {
397
+ hookLog('require-work-item', 'PreToolUse', 'pass-out-of-scope', { cwd, scope });
398
+ return process.exit(0);
399
+ }
400
+
388
401
  if (!(await requiresWorkItemNow())) {
389
402
  hookLog('require-work-item', 'PreToolUse', 'pass-flow-exempt', {});
390
403
  return process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.37.8",
3
+ "version": "0.37.10",
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.8",
22
+ "version": "0.37.10",
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
- if (n.type !== 'call_expression') return;
427
- const fn = n.childForFieldName('function');
428
- if (fn) out.push(fn.text.replace(/^this\./, ''));
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.
@@ -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`. Register the
60
- target, then open it again — never guess a default and never fall back to
61
- hand-rolled git.
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
- | class | build | dev | prod |
162
- |---|---|---|---|
163
- | `apps/<name>` | `pnpm --filter @regen/<name> build` | PM2 @ Vultr | Coolify |
164
- | `packages/<name>` | `pnpm --filter @regen/<name> build` | imported | `npm publish` |
165
- | `sites/`, `models/` | static/vite | PM2 @ Vultr | Coolify |
166
- | standalone | its own tooling | — | Coolify |
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