@ericrisco/rsc 1.3.3 → 1.3.5

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/README.md CHANGED
@@ -216,6 +216,11 @@ Accept this exact harness plan?
216
216
  The terminal and chat adapters produce the same normalized answers and plan id. If project evidence
217
217
  changes between preview and acceptance, rsc returns `RSC_PLAN_CHANGED` and writes nothing. After an
218
218
  accepted application it verifies the receipt and managed state before printing `RSC_ONBOARDING_READY`.
219
+ That verdict is not just about the receipt: it also requires the harness floor to exist —
220
+ `01-TOOLS/_TEMPLATE/`, `02-DOCS/wiki/harness/`, and the constitution when the plan selects SDD.
221
+ If the floor is missing, the install applied but prints `RSC_ONBOARDING_INCOMPLETE` with each
222
+ missing path and the action that creates it, because a plan that promised little used to be able to
223
+ report success with three markdown files.
219
224
 
220
225
  ---
221
226
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.3.3",
2
+ "version": "1.3.5",
3
3
  "counts": {
4
4
  "skills": 272,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, writeFileSync } from 'node:fs';
3
- import { join, relative, resolve, sep } from 'node:path';
2
+ import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { applyInstall, pruneSharedBases, removeTargetInstall } from '../install-apply.js';
5
6
  import { targetPaths } from '../../targets/index.js';
6
7
  import { readState } from './state.js';
@@ -204,3 +205,158 @@ export async function applyAcceptedOnboarding({ cwd = process.cwd(), plan, planI
204
205
  throw new Error(`RSC_ONBOARDING_INCOMPLETE: ${error.message}. Recover with: ${recoveryCommand(plan, planId)}`);
205
206
  }
206
207
  }
208
+
209
+ // ─────────────────────────────────────────────────────────────────────────────
210
+ // El suelo del arnés: qué tiene que existir para que decir «listo» sea verdad.
211
+ //
212
+ // «Completado» estaba definido de forma RELATIVA — el estado real coincide con lo que el plan
213
+ // prometió — así que un plan que promete poco se cumplía entero, y una instalación podía imprimir
214
+ // RSC_ONBOARDING_READY con tres markdown y un `.rsc.json`. `01-TOOLS/` no existía para el
215
+ // instalador y la constitución no se mencionaba en ningún sitio.
216
+ //
217
+ // Spec: 02-DOCS/wiki/sdd/specs/install-completion-floor.md
218
+ //
219
+ // Tres cosas que este código NO hace, y cada una es una redacción del plan que se estrelló:
220
+ //
221
+ // 1. NO se comprueba dentro de `applyAcceptedOnboarding`. Ese camino revierte ante cualquier
222
+ // diferencia, y parte del suelo la produce una fase agéntica posterior: comprobarlo ahí
223
+ // convertía cada instalación nueva en un rollback. Medido, no razonado.
224
+ // 2. NO cuelga de `hasDeclaredHarness`. Ese booleano enruta `add`, `install` y el wizard, así que
225
+ // un suelo incompleto habría devuelto RSC_ONBOARDING_REQUIRED y bloqueado la Fase 3 de `init`.
226
+ // El suelo impide AFIRMAR que el arnés está listo; no impide nada más.
227
+ // 3. NO compara contenido. `01-TOOLS/` es la capa que el usuario modifica al añadir herramientas
228
+ // y donde viven los `.env`: fijar su contenido convertiría el uso normal en un fallo.
229
+ //
230
+ // Y la retro-compatibilidad no es una lista de exentos (P3): el suelo viaja DENTRO del plan, así
231
+ // que un recibo aceptado por una versión anterior no lo trae y queda exento por construcción.
232
+
233
+ const LIB_DIR = dirname(fileURLToPath(import.meta.url));
234
+ const TEMPLATE_SOURCE = join(LIB_DIR, '..', '..', 'skills', 'harness', 'assets', '_TEMPLATE');
235
+ const TEMPLATE_FLOOR = '01-TOOLS/_TEMPLATE/';
236
+
237
+ export const HARNESS_FLOOR_MINIMUM = [TEMPLATE_FLOOR, '02-DOCS/wiki/harness/'];
238
+ export const HARNESS_FLOOR_CONSTITUTION = '02-DOCS/wiki/sdd/constitution.md';
239
+
240
+ // Los ficheros que la plantilla trae hoy. Se lee del asset en vez de codificar una lista, para que
241
+ // añadir un fichero a la plantilla no deje el suelo comprobando de menos en silencio.
242
+ //
243
+ // Un error aquí NO se convierte en lista vacía. Comérselo dejaba el suelo permanentemente
244
+ // insatisfacible, con un mensaje que ofrecía una acción que leía ese mismo asset ausente y que por
245
+ // tanto no podía arreglarlo nunca — y sin nombrar jamás la causa real, que es un paquete roto.
246
+ function templateAssets() {
247
+ return readdirSync(TEMPLATE_SOURCE).sort();
248
+ }
249
+
250
+ // npm NUNCA empaqueta un fichero llamado `.gitignore`, así que el asset viaja sin punto y se copia
251
+ // con él. Sin esto, desde el paquete publicado se copiaban 4 de 5 ficheros y el que faltaba era el
252
+ // único que evita comitear el `.env` que el README —copiado por el propio instalador— manda crear.
253
+ const DOTTED = { gitignore: '.gitignore' };
254
+ const targetName = (asset) => DOTTED[asset] ?? asset;
255
+
256
+ // La capa entera, no cada proveedor: el flujo documentado es copiar `_TEMPLATE/` a
257
+ // `01-TOOLS/<PROVEEDOR>/`, y depender de que cada copia se lleve su propia protección es depender
258
+ // de que nadie se salte un paso con credenciales de por medio.
259
+ const LAYER_IGNORE = ['# Escrito por rsc: la capa de herramientas guarda credenciales.',
260
+ '*/.env', '*/.env.*', '!*/.env.example', '*/keys/', '*/out/', ''].join('\n');
261
+
262
+ /**
263
+ * Crea la parte determinista del esqueleto. Copiar ficheros estáticos es un algoritmo, así que por
264
+ * P1 le toca al binario en vez de pedírsela a una fase agéntica que puede no llegar.
265
+ *
266
+ * Se llama DESPUÉS de que la transacción del apply confirme: el suelo no está en `governedPaths`
267
+ * —no debe estarlo, o se le calcularía digest— así que tampoco está en el snapshot, y crearlo dentro
268
+ * de la transacción dejaría residuo si el apply revirtiera.
269
+ *
270
+ * Idempotente y auto-reparadora POR FICHERO: un `_TEMPLATE/` a medias queda completo al reaplicar, y
271
+ * un fichero que el usuario haya tocado no se sobrescribe nunca.
272
+ */
273
+ export function ensureHarnessSkeleton(cwd = process.cwd()) {
274
+ // La guarda existía en este mismo fichero y no se llamaba. `existsSync` sigue los enlaces, así que
275
+ // un `01-TOOLS` o un `_TEMPLATE` que fuese symlink apuntaba el `copyFileSync` fuera de la raíz — y
276
+ // git guarda los symlinks, así que el vector viaja en un clone. El repo ya fija la política
277
+ // contraria para los caminos gobernados, con dos tests que exigen que nada se escriba fuera.
278
+ assertManagedPathsStayInsideRoot(cwd, ['01-TOOLS/', TEMPLATE_FLOOR, '02-DOCS/wiki/harness/']);
279
+ const created = [];
280
+ for (const dir of ['02-DOCS/wiki/harness', '01-TOOLS/_TEMPLATE']) {
281
+ const absolute = join(cwd, dir);
282
+ // lstatSync y no existsSync: un enlace «existe» y nos habría hecho seguir escribiendo a través.
283
+ if (existsSync(absolute) && !lstatSync(absolute).isSymbolicLink()) continue;
284
+ mkdirSync(absolute, { recursive: true });
285
+ created.push(`${dir}/`);
286
+ }
287
+ for (const asset of templateAssets()) {
288
+ const target = join(cwd, '01-TOOLS', '_TEMPLATE', targetName(asset));
289
+ if (existsSync(target)) continue;
290
+ copyFileSync(join(TEMPLATE_SOURCE, asset), target);
291
+ created.push(`${TEMPLATE_FLOOR}${targetName(asset)}`);
292
+ }
293
+ const layer = join(cwd, '01-TOOLS', '.gitignore');
294
+ if (!existsSync(layer)) {
295
+ writeFileSync(layer, LAYER_IGNORE);
296
+ created.push('01-TOOLS/.gitignore');
297
+ }
298
+ return { created };
299
+ }
300
+
301
+ // Un camino declarado se compara por su FORMA, no por su texto: `01-TOOLS/_TEMPLATE` sin barra, con
302
+ // `./` delante o con doble barra degradaban la comprobación por fichero a simple existencia de
303
+ // directorio, que es el modo de fallo que este suelo existe para evitar, alcanzable con un carácter.
304
+ const floorSegments = (path) => String(path).split(/[/\\]+/).filter((part) => part && part !== '.');
305
+ const floorKey = (path) => floorSegments(path).join('/');
306
+ const TEMPLATE_KEY = floorKey(TEMPLATE_FLOOR);
307
+
308
+ // Un directorio que existe y está vacío pasa cualquier comprobación de existencia y no sirve para
309
+ // nada — el mismo modo de fallo por el que el suelo es `01-TOOLS/_TEMPLATE/` y no `01-TOOLS/`, un
310
+ // nivel más abajo.
311
+ function floorSatisfied(cwd, path) {
312
+ const key = floorKey(path);
313
+ const absolute = join(cwd, key);
314
+ if (!existsSync(absolute)) return false;
315
+ if (key !== TEMPLATE_KEY) return true;
316
+ const assets = templateAssets();
317
+ // `assets.length > 0` es cinturón y tirantes, y hoy es INALCANZABLE: `templateAssets()` lanza si no
318
+ // puede leer el asset, así que sólo daría cero con un asset legítimamente vacío — un paquete roto
319
+ // de otra forma. Se declara en vez de fingir que un test lo cubre: el mutante que la quita
320
+ // sobrevive la suite, y es equivalente, no un hueco.
321
+ return assets.length > 0 && assets.every((asset) => existsSync(join(absolute, targetName(asset))));
322
+ }
323
+
324
+ /** Qué falta del suelo. Puro, sólo existencia, y vacío para un recibo que no declara suelo. */
325
+ // `floorPaths` sale del recibo persistido en `.rsc.json`: un fichero comiteado, propenso a
326
+ // conflictos de merge, y que un repo clonado puede traer preparado. Así que se valida la forma antes
327
+ // de mirar el disco: un `..` daba el suelo por satisfecho MIRANDO FUERA de la raíz, y un `null` o una
328
+ // cadena en lugar de un array tumbaban `rsc install` después de haber instalado.
329
+ export function missingHarnessFloor(cwd = process.cwd(), plan = {}) {
330
+ const declared = Array.isArray(plan?.floorPaths) ? plan.floorPaths : [];
331
+ const missing = [];
332
+ for (const path of declared) {
333
+ const segments = typeof path === 'string' ? floorSegments(path) : [];
334
+ const usable = segments.length > 0 && !segments.includes('..') && !String(path).startsWith('/');
335
+ // Un camino que no se puede usar se reporta como NO satisfecho, nunca como satisfecho, y sin
336
+ // repetir su contenido: es dato ajeno y va a un canal que lee un agente.
337
+ if (!usable) { missing.push('missing harness floor <invalid declaration>'); continue; }
338
+ // La barra sólo si el camino declarado la traía: el suelo condicional es un FICHERO, y decir
339
+ // `constitution.md/` invita a buscar un directorio que no existe.
340
+ const shown = String(path).endsWith('/') ? `${floorKey(path)}/` : floorKey(path);
341
+ if (!floorSatisfied(cwd, path)) missing.push(`missing harness floor ${shown}`);
342
+ }
343
+ return missing;
344
+ }
345
+
346
+ /**
347
+ * Lo que el instalador puede afirmar honestamente, y qué hacer si no.
348
+ *
349
+ * `action` tiene que poder CREAR lo que falta. Hoy el `Recover with:` del apply reejecuta el
350
+ * onboarding, y el onboarding no puede montar el esqueleto: una recuperación que no recupera
351
+ * consume el único intento que el usuario iba a hacer.
352
+ */
353
+ export function harnessReadiness(cwd = process.cwd(), plan = {}) {
354
+ const missing = missingHarnessFloor(cwd, plan);
355
+ return {
356
+ ready: missing.length === 0,
357
+ missing,
358
+ action: missing.length
359
+ ? 'invoke the `harness` skill to scaffold 01-TOOLS/ + 02-DOCS/, and the `constitution` phase when SDD is selected'
360
+ : '',
361
+ };
362
+ }
@@ -74,6 +74,20 @@ export function scanProject(root = process.cwd()) {
74
74
  const rel = relative(absolute, path).split(sep).join('/');
75
75
  if (rel.startsWith('../') || rel === '..') throw new Error('project scan escaped the selected root');
76
76
  if (rel === '02-DOCS/wiki/harness' || rel.startsWith('02-DOCS/wiki/harness/')) continue;
77
+ // Mismo motivo que la línea de arriba, y descubierto igual de tarde: el instalador escribe
78
+ // `01-TOOLS/_TEMPLATE/` y sus dos `.md` entraban como evidencia de proyecto, moviendo la
79
+ // identidad del plan. El segundo `onboard --accept-plan <id>` moría con RSC_PLAN_CHANGED, y
80
+ // con él el reintento que la documentación pide tras reparar el suelo. Los proveedores reales
81
+ // que el usuario añada bajo `01-TOOLS/` sí siguen contando: son suyos, no nuestros.
82
+ if (rel === '01-TOOLS/_TEMPLATE' || rel.startsWith('01-TOOLS/_TEMPLATE/')) continue;
83
+ // Y los artefactos de la propia cadena SDD, por el mismo invariante y por una razón que costó
84
+ // un release descubrir: la constitución es un `.md`, así que contaba como evidencia de
85
+ // proyecto. Eso movía la identidad del plan justo después de que el agente la escribiera —
86
+ // obedeciendo a `site/llms.txt`, que le manda reparar el suelo y reintentar hasta obtener
87
+ // READY— y hacía ese reintento imposible. Arreglar sólo `01-TOOLS/_TEMPLATE/` tapó la mitad
88
+ // del agujero: la que produce el instalador, no la que produce la reparación.
89
+ // `inbox/` y `raw/` NO se excluyen: eso es material del usuario y sí es evidencia.
90
+ if (rel === '02-DOCS/wiki/sdd' || rel.startsWith('02-DOCS/wiki/sdd/')) continue;
77
91
  if (entry.isDirectory()) { visit(path); continue; }
78
92
  if (!entry.isFile()) continue;
79
93
  let isSignal = manifests.has(entry.name) || entry.name.endsWith('.md');
@@ -219,6 +233,16 @@ export function buildOnboardingPlan(record, evidence) {
219
233
  ...normalized.targets.flatMap((target) => managedPathsForInstall({ skillIds: skills, target, cwd: root, policy })
220
234
  .map((path) => relative(root, path).split(sep).join('/'))),
221
235
  ])].sort() : ['.rsc.json', '.rsc/', '02-DOCS/wiki/harness/'];
236
+ // El suelo viaja dentro del plan a propósito: así un recibo aceptado por una versión anterior no
237
+ // lo trae y queda exento sin que exista ninguna lista de exentos (P3, el contenido es el ledger).
238
+ // La constitución entra SÓLO si el plan eligió SDD, porque los criterios 5 y 17 de la spec de
239
+ // onboarding la posponen a propósito en workspaces no-software y en software pequeño.
240
+ const floorPaths = [
241
+ '01-TOOLS/_TEMPLATE/',
242
+ '02-DOCS/wiki/harness/',
243
+ ...(decisions.some((decision) => decision.id === 'sdd' && decision.state === 'selected')
244
+ ? ['02-DOCS/wiki/sdd/constitution.md'] : []),
245
+ ].sort();
222
246
  return {
223
247
  schemaVersion: 1,
224
248
  record: normalized,
@@ -231,6 +255,7 @@ export function buildOnboardingPlan(record, evidence) {
231
255
  decisions,
232
256
  policy,
233
257
  governedPaths,
258
+ floorPaths,
234
259
  };
235
260
  }
236
261
 
package/scripts/rsc.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  normalizeOnboarding, missingOnboardingFields, scanProject,
21
21
  buildOnboardingPlan, decodeGoal, encodeGoal, identifyPlan, recommendDeferredComponents,
22
22
  } from './lib/onboarding.js';
23
- import { applyAcceptedOnboarding, verifyOnboarding } from './lib/onboarding-apply.js';
23
+ import { applyAcceptedOnboarding, verifyOnboarding, ensureHarnessSkeleton, harnessReadiness } from './lib/onboarding-apply.js';
24
24
 
25
25
  const rawArgv = process.argv.slice(2);
26
26
  const GLOBAL_VALUE_FLAGS = new Set([
@@ -200,11 +200,46 @@ async function runOnboarding(targets) {
200
200
  }
201
201
  try {
202
202
  await applyAcceptedOnboarding({ cwd: process.cwd(), plan, planId });
203
- say(`RSC_ONBOARDING_READY ${planId}`);
204
203
  } catch (error) {
205
204
  console.error(error.message);
206
205
  process.exitCode = 4;
206
+ return;
207
+ }
208
+ // A partir de aquí la transacción YA confirmó, así que nada de lo que siga puede reportarse como
209
+ // fallo de aplicación: decirlo mentiría en la otra dirección, sobre una instalación que sí está
210
+ // aplicada. Montar el esqueleto va fuera de la transacción por eso mismo — dentro dejaría residuo
211
+ // si el apply revirtiera, porque el suelo no está en `governedPaths` y por tanto no se snapshotea.
212
+ try {
213
+ ensureHarnessSkeleton(process.cwd());
214
+ } catch (error) {
215
+ say(`RSC_SKELETON_FAILED ${error.message}`);
207
216
  }
217
+ emitHarnessReadiness(plan, planId);
218
+ }
219
+
220
+ // La única línea que afirmaba que el arnés estaba listo lo hacía sin mirar si existía: «completado»
221
+ // estaba definido contra lo que el plan prometió, no contra un suelo, así que un plan que prometía
222
+ // poco se cumplía entero. Spec: 02-DOCS/wiki/sdd/specs/install-completion-floor.md
223
+ function emitHarnessReadiness(plan, planId) {
224
+ const readiness = harnessReadiness(process.cwd(), plan);
225
+ if (readiness.ready) return void say(`RSC_ONBOARDING_READY ${planId}`);
226
+ say(`RSC_ONBOARDING_INCOMPLETE ${planId}`);
227
+ for (const missing of safeLines(readiness.missing)) say(` ${missing}`);
228
+ say(` Next: ${readiness.action}`);
229
+ }
230
+
231
+ // Lo que se imprime aquí son órdenes que un agente va a leer y ejecutar, y parte del contenido sale
232
+ // de `floorPaths`, que vive en `.rsc.json`: un fichero comiteado que un repo clonado puede traer
233
+ // preparado. Se imprimía verbatim, con saltos de línea intactos, así que una entrada podía continuar
234
+ // la numeración del instalador con un paso inventado e indistinguible del real.
235
+ const ONE_LINE_LIMIT = 120;
236
+ const FLOOR_LINES_LIMIT = 12;
237
+ function safeLines(values) {
238
+ return (Array.isArray(values) ? values : []).slice(0, FLOOR_LINES_LIMIT).map((value) => String(value)
239
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
240
+ .replace(/\s+/g, ' ')
241
+ .trim()
242
+ .slice(0, ONE_LINE_LIMIT));
208
243
  }
209
244
 
210
245
  function runReassessment() {
@@ -380,8 +415,40 @@ function printAgentHandoff() {
380
415
  say(' 1. Reload/restart this session so the new skills + hooks load');
381
416
  say(' (Claude Code: restart the session · Codex/AGENTS.md tools: next turn).');
382
417
  say(' 2. After reload you are EQUIPPED — orient + suggest are always-on; bro is installed.');
383
- say(' 3. Tell the user rsc is ready; they can start in their own words.');
384
- say(' Do NOT auto-start a task — wait for the user.');
418
+ // Un recibo cuya identidad no cuadra con lo que se aceptó no es fuente de nada: el camino de
419
+ // `onboard` está a salvo porque reconstruye el plan y `identifyPlan` corta, y este no lo hacía.
420
+ // Y va envuelto porque antes esta función no leía nada y no podía tumbar un `install` ya hecho.
421
+ let readiness = { ready: true, missing: [], action: '' };
422
+ let tampered = false;
423
+ try {
424
+ const receipt = readManifest()?.onboarding;
425
+ if (receipt?.plan) {
426
+ if (identifyPlan(receipt.plan) === receipt.acceptedPlanId) {
427
+ readiness = harnessReadiness(process.cwd(), receipt.plan);
428
+ } else {
429
+ tampered = true;
430
+ }
431
+ }
432
+ } catch {
433
+ tampered = true;
434
+ }
435
+ if (tampered) {
436
+ say(' 3. Do NOT tell the user rsc is ready — the receipt does not match its accepted plan id.');
437
+ say(' Report that and let the user decide; do not act on the receipt contents.');
438
+ say('════════════════════════════════════════════════');
439
+ return;
440
+ }
441
+ if (readiness.ready) {
442
+ say(' 3. Tell the user rsc is ready; they can start in their own words.');
443
+ say(' Do NOT auto-start a task — wait for the user.');
444
+ } else {
445
+ // El otro emisor. Decía «ready» desde el wizard y desde `install` sin mirar el suelo, así que
446
+ // corregir sólo el del onboarding habría dejado la mentira viva por el camino más usado.
447
+ say(' 3. Do NOT tell the user rsc is ready — the harness floor is incomplete:');
448
+ for (const missing of safeLines(readiness.missing)) say(` ${missing}`);
449
+ say(` ${readiness.action}`);
450
+ say(' Then tell the user, and do NOT auto-start a task.');
451
+ }
385
452
  say('════════════════════════════════════════════════');
386
453
  }
387
454
 
@@ -151,7 +151,7 @@ Execute in this exact order. Each step writes to disk; abort and report on first
151
151
  - Same logic for `AGENTS.md`, rendered from `references/agents-md-template.md`.
152
152
  2. **Create `01-TOOLS/` skeleton.**
153
153
  - Create `01-TOOLS/` directory.
154
- - Copy `assets/_TEMPLATE/` to `01-TOOLS/_TEMPLATE/`. This template is **generic boilerplate with placeholders (`<NOMBRE_TOOL>`, `<TOOL>_API_KEY`)**. The user copies it manually when adding a tool NOT in the catalog. The skill itself does NOT use `_TEMPLATE/` to generate the detected tools — those come from `providers.yaml`.
154
+ - Copy `assets/_TEMPLATE/` to `01-TOOLS/_TEMPLATE/`. The asset ships its ignore rules as `gitignore` (no leading dot, because npm never packages a `.gitignore`) and it must land as `.gitignore`; the installer already does this on every apply, so normally you will find the directory built. This template is **generic boilerplate with placeholders (`<NOMBRE_TOOL>`, `<TOOL>_API_KEY`)**. The user copies it manually when adding a tool NOT in the catalog. The skill itself does NOT use `_TEMPLATE/` to generate the detected tools — those come from `providers.yaml`.
155
155
  3. **Per detected tool** (in catalog order):
156
156
  - Create `01-TOOLS/<ID>/`.
157
157
  - Write every file from the provider entry's `files:` map verbatim (replacing template variables: `{{TOOL_ID}}`, `{{DASHBOARD_URL}}`, etc.).
@@ -0,0 +1,10 @@
1
+ .env
2
+ keys/
3
+ out/
4
+ *.p8
5
+ *.p12
6
+ *.jks
7
+ *.json
8
+ !*.example.json
9
+ __pycache__/
10
+ *.pyc
@@ -173,13 +173,26 @@ means the user starts clean.
173
173
 
174
174
  ### Phase 5 — HANDOFF
175
175
 
176
- `init` stops here. It has set the profile, recorded the discovery, and installed the skills.
177
-
178
- > "Tu perfil y lo que hemos hablado ya están guardados. Ahora ejecuta `harness` y monto el esqueleto
179
- > del proyecto (`01-TOOLS/` + `02-DOCS/`) leyendo todo lo que acabamos de decidir."
180
-
181
- Do not scaffold while you are here, however tempting — `harness` reads this same profile and owns
182
- that job.
176
+ The profile is set, the discovery recorded, the skills installed. What is missing is the workspace
177
+ itself, and this is where onboarding used to end with a request: *"now run `harness`"*. Nothing ran
178
+ it. Finishing depended on the user reading a line and typing something — and in a chat install that
179
+ user has just delegated precisely so they would not have to know what to type.
180
+
181
+ So **invoke `harness` yourself, now**, as the last step of this phase. Say what you are doing in one
182
+ line, then do it:
183
+
184
+ > "Tu perfil y lo que hemos hablado ya están guardados. Monto ahora el esqueleto del proyecto
185
+ > (`01-TOOLS/` + `02-DOCS/`) leyendo todo lo que acabamos de decidir."
186
+
187
+ `harness` still owns *how* the scaffold is built — you are calling it, not reimplementing it. Do not
188
+ hand-roll directories here.
189
+
190
+ **And if it does not happen, say so.** If `harness` cannot run, or stops half-way, **do not tell the
191
+ user rsc is ready**: name what is missing and what will create it. The installer holds the same rule
192
+ — it prints `RSC_ONBOARDING_INCOMPLETE` instead of `RSC_ONBOARDING_READY` when the harness floor is
193
+ absent — and an agent that contradicts its own installer is worse than either alone. Claiming a
194
+ harness that is not there is the one failure the user cannot diagnose, because everything looks
195
+ installed and nothing behaves as if it were.
183
196
 
184
197
  ## Project grounding
185
198