@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/git-sha.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "sha": "9621389d4e4724ae260e0d4e91651c10b3ac0e2c"
2
+ "sha": "74680b31a041d74e9c4c72e58cbc876fd5e920c3"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.37.7",
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.7",
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
- 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
  }
@@ -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 |
@@ -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 is READ-ONLY. It cannot write to any lane — see below.** Never dispatch it from your own lane. | 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. |
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.