@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 +5 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/lib/onboarding-apply.js +158 -2
- package/scripts/lib/onboarding.js +25 -0
- package/scripts/rsc.js +71 -4
- package/skills/harness/SKILL.md +1 -1
- package/skills/harness/assets/_TEMPLATE/gitignore +10 -0
- package/skills/init/SKILL.md +20 -7
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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
384
|
-
|
|
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
|
|
package/skills/harness/SKILL.md
CHANGED
|
@@ -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.).
|
package/skills/init/SKILL.md
CHANGED
|
@@ -173,13 +173,26 @@ means the user starts clean.
|
|
|
173
173
|
|
|
174
174
|
### Phase 5 — HANDOFF
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|