@ericrisco/rsc 3.0.7 → 3.0.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/README.md +27 -12
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/audit.js +11 -0
- package/scripts/doctor.js +69 -11
- package/scripts/install-apply.js +221 -18
- package/scripts/lib/constitution-draft.js +156 -0
- package/scripts/lib/onboarding-apply.js +31 -25
- package/scripts/lib/onboarding.js +26 -4
- package/scripts/lib/tools-skeleton.js +76 -0
- package/scripts/rsc.js +109 -14
- package/skills/constitution/SKILL.md +2 -0
- package/skills/ftd/SKILL.md +9 -0
- package/skills/sdd/SKILL.md +2 -2
- package/skills/specify/SKILL.md +1 -1
- package/skills/suggest/SKILL.md +5 -4
- package/targets/branch-guard.mjs +94 -26
- package/targets/claude.js +36 -6
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/danger-guard.mjs +80 -14
- package/targets/ftd-nudge.mjs +137 -0
- package/targets/gitmoji-guard.mjs +14 -1
- package/targets/knowledge-sync.mjs +490 -48
- package/targets/opt-outs.js +2 -0
- package/targets/sello.mjs +9 -2
- package/targets/session-memory-adapter.mjs +27 -3
- package/targets/session-memory-core.mjs +219 -36
- package/targets/session-start.mjs +40 -5
- package/targets/shell-unwrap.mjs +145 -0
- package/targets/ship-guard.mjs +40 -9
- package/targets/team-safe-start.mjs +76 -10
- package/targets/worktree-reaper.mjs +114 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// La constitución como BORRADOR.
|
|
6
|
+
//
|
|
7
|
+
// El suelo del arnés exige `02-DOCS/wiki/sdd/constitution.md` cuando el plan elige SDD, y sólo una
|
|
8
|
+
// fase agéntica la escribía. Así que una instalación no interactiva de software complejo —terminal,
|
|
9
|
+
// CI, nadie con un agente delante— terminaba en RSC_ONBOARDING_INCOMPLETE: medio instalada. En la
|
|
10
|
+
// prueba de campo una sesión entera ($3.59, 67 turnos) se gastó sólo en escribirla.
|
|
11
|
+
//
|
|
12
|
+
// Decisión del PO (2026-10-07): el onboarding escribe un BORRADOR y no falla. El borrador lleva
|
|
13
|
+
// `status: draft`, los encabezados reales de la plantilla de la fase `constitution`, y sólo los
|
|
14
|
+
// hechos que el onboarding conoce — objetivo, tipo, alcance, flujo, evidencia de stack. Ningún
|
|
15
|
+
// principio: ratificar es de la fase `constitution`, con el usuario delante (P4). El suelo lo acepta
|
|
16
|
+
// porque sólo mira existencia; la cadena SDD lo detecta por `status: draft` y lo completa antes del
|
|
17
|
+
// primer spec; `doctor` lo lista como pendiente, nunca como fallo.
|
|
18
|
+
|
|
19
|
+
export const CONSTITUTION_PATH = '02-DOCS/wiki/sdd/constitution.md';
|
|
20
|
+
export const DRAFT_MARK = 'borrador — complétalo con la fase `constitution` antes del primer SDD';
|
|
21
|
+
|
|
22
|
+
const WORKFLOW_FACT = {
|
|
23
|
+
branches: 'branches closed with a pull request; the default branch is closed to direct work',
|
|
24
|
+
main: 'straight on the default branch',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// El objetivo es texto del usuario y va a un documento que leerá un agente: una línea, acotada.
|
|
28
|
+
const oneLine = (value, limit = 200) => String(value ?? '').replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
29
|
+
.replace(/\s+/g, ' ').trim().slice(0, limit);
|
|
30
|
+
|
|
31
|
+
/** Puro: el texto del borrador a partir del plan aceptado. Sin reloj ni disco, salvo lo que se le pase. */
|
|
32
|
+
export function renderConstitutionDraft(plan, { project = 'Project', date = new Date() } = {}) {
|
|
33
|
+
const record = plan?.record || {};
|
|
34
|
+
const evidence = plan?.evidence || {};
|
|
35
|
+
const stacks = (evidence.stacks || []).map((s) => oneLine(s, 40)).filter(Boolean);
|
|
36
|
+
const manifests = (evidence.signals || []).filter((s) => String(s).startsWith('manifest:'))
|
|
37
|
+
.map((s) => oneLine(String(s).slice('manifest:'.length), 60));
|
|
38
|
+
const day = date.toISOString().slice(0, 10);
|
|
39
|
+
const name = oneLine(project, 80) || 'Project';
|
|
40
|
+
const stackLine = stacks.length
|
|
41
|
+
? `Detected at install: ${stacks.join(', ')}${manifests.length ? ` (from ${manifests.join(', ')})` : ''}. Not ratified yet.`
|
|
42
|
+
: 'No stack evidence was detected at install. To be decided in the `constitution` phase.';
|
|
43
|
+
const workflow = WORKFLOW_FACT[record.workflow];
|
|
44
|
+
const pending = '_Pending — to be completed in the `constitution` phase._';
|
|
45
|
+
return `---
|
|
46
|
+
type: constitution
|
|
47
|
+
title: ${name} — Constitution
|
|
48
|
+
description: Draft written by rsc onboard from the accepted plan; not ratified.
|
|
49
|
+
tags: [sdd, constitution]
|
|
50
|
+
timestamp: ${date.toISOString().replace(/\.\d{3}Z$/, 'Z')}
|
|
51
|
+
topic: sdd
|
|
52
|
+
status: draft
|
|
53
|
+
version: draft
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
# ${name} — Constitution
|
|
57
|
+
|
|
58
|
+
> **${DRAFT_MARK}.**
|
|
59
|
+
> Draft: complete it with the \`constitution\` phase before the first SDD feature. It holds only what
|
|
60
|
+
> onboarding knew; it ratifies no principle. Ratifying it removes \`status: draft\` and starts at v1.0.0.
|
|
61
|
+
|
|
62
|
+
## Known facts (from onboarding)
|
|
63
|
+
|
|
64
|
+
- Goal: ${oneLine(record.goal)}
|
|
65
|
+
- Project kind: ${oneLine(record.projectKind, 40)}${record.softwareScope ? ` · software scope: ${oneLine(record.softwareScope, 40)}` : ''}
|
|
66
|
+
${workflow ? `- How code changes land: ${workflow} (\`--workflow ${record.workflow}\`).\n` : ''}- Stack evidence: ${stackLine}
|
|
67
|
+
|
|
68
|
+
## 1. Stack canon
|
|
69
|
+
|
|
70
|
+
${stackLine}
|
|
71
|
+
|
|
72
|
+
## 2. Quality bar
|
|
73
|
+
|
|
74
|
+
${pending}
|
|
75
|
+
|
|
76
|
+
## 3. Conventions
|
|
77
|
+
|
|
78
|
+
${pending}
|
|
79
|
+
|
|
80
|
+
## 4. Branching & shipping
|
|
81
|
+
|
|
82
|
+
${workflow ? `Chosen at install: ${workflow}. ` : ''}${pending}
|
|
83
|
+
|
|
84
|
+
## 5. Security & privacy floor
|
|
85
|
+
|
|
86
|
+
${pending}
|
|
87
|
+
|
|
88
|
+
## 6. UX / accessibility floor (if there is a UI)
|
|
89
|
+
|
|
90
|
+
${pending}
|
|
91
|
+
|
|
92
|
+
## 7. Performance budgets (where they matter)
|
|
93
|
+
|
|
94
|
+
${pending}
|
|
95
|
+
|
|
96
|
+
## 8. Knowledge & decisions
|
|
97
|
+
|
|
98
|
+
${pending}
|
|
99
|
+
|
|
100
|
+
## Definition of Done (the merge bar \`verify\` runs against)
|
|
101
|
+
|
|
102
|
+
${pending}
|
|
103
|
+
|
|
104
|
+
## Amendment log (append-only)
|
|
105
|
+
|
|
106
|
+
| Date | Version | Change | Why |
|
|
107
|
+
|------|---------|--------|-----|
|
|
108
|
+
| ${day} | draft | Draft generated by \`rsc onboard\`. | The SDD floor needs a constitution; ratification is the \`constitution\` phase's. |
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** ¿La constitución existe y está en borrador? Sólo lee el frontmatter; ilegible ⇒ no es borrador. */
|
|
113
|
+
export function constitutionIsDraft(cwd = process.cwd()) {
|
|
114
|
+
const path = join(cwd, CONSTITUTION_PATH);
|
|
115
|
+
try {
|
|
116
|
+
if (!existsSync(path) || !lstatSync(path).isFile()) return false;
|
|
117
|
+
const text = readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
|
|
118
|
+
const block = /^---\n([\s\S]*?)\n---(\n|$)/.exec(text);
|
|
119
|
+
return Boolean(block && /^status:\s*["']?draft["']?\s*$/m.test(block[1]));
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isLink = (path) => { try { return lstatSync(path).isSymbolicLink(); } catch { return false; } };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Escribe el borrador si el plan exige la constitución y no hay ninguna. Nunca sobrescribe: ni una
|
|
129
|
+
* ratificada ni un borrador que el usuario ya empezó. Un enlace (aunque cuelgue) cuenta como
|
|
130
|
+
* existente, y `wx` cierra la carrera entre comprobar y escribir.
|
|
131
|
+
*/
|
|
132
|
+
export function ensureConstitutionDraft(cwd, plan, { now = new Date() } = {}) {
|
|
133
|
+
if (!(plan?.floorPaths || []).includes(CONSTITUTION_PATH)) return { created: false };
|
|
134
|
+
// Sin importar `onboarding-apply.js` (doctor lee este módulo y no debe arrastrar el instalador):
|
|
135
|
+
// un directorio intermedio que sea enlace podría sacar la escritura de la raíz, y git versiona los
|
|
136
|
+
// enlaces, así que el vector viaja en un clone. Más estricto que la guarda general: no se sigue.
|
|
137
|
+
let cursor = cwd;
|
|
138
|
+
for (const segment of ['02-DOCS', 'wiki', 'sdd']) {
|
|
139
|
+
cursor = join(cursor, segment);
|
|
140
|
+
if (existsSync(cursor) || isLink(cursor)) {
|
|
141
|
+
if (lstatSync(cursor).isSymbolicLink()) throw new Error(`RSC_ROOT_AMBIGUOUS: ${CONSTITUTION_PATH} would be written through a symlink`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const target = join(cwd, CONSTITUTION_PATH);
|
|
145
|
+
let present = false;
|
|
146
|
+
try { lstatSync(target); present = true; } catch { /* ausente */ }
|
|
147
|
+
if (present) return { created: false };
|
|
148
|
+
mkdirSync(join(cwd, '02-DOCS', 'wiki', 'sdd'), { recursive: true });
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(target, renderConstitutionDraft(plan, { project: basename(cwd), date: now }), { flag: 'wx' });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === 'EEXIST') return { created: false };
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
return { created: true };
|
|
156
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { applyInstall, pruneSharedBases, removeTargetInstall } from '../install-apply.js';
|
|
6
6
|
import { targetPaths } from '../../targets/index.js';
|
|
7
7
|
import { targetHasAgents } from '../../targets/agents.js';
|
|
@@ -10,6 +10,8 @@ import { readManifest, writeManifest } from './manifest-file.js';
|
|
|
10
10
|
import { encodeGoal, identifyPlan } from './onboarding.js';
|
|
11
11
|
import { createBackup, restoreBackup } from './backups.js';
|
|
12
12
|
import { RETIRED_SKILLS, replaceRetired } from './retired-skills.js';
|
|
13
|
+
import { CONSTITUTION_PATH } from './constitution-draft.js';
|
|
14
|
+
import { LAYER_IGNORE, TEMPLATE_SOURCE, targetName, templateAssets } from './tools-skeleton.js';
|
|
13
15
|
|
|
14
16
|
const sameSet = (a = [], b = []) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
|
|
15
17
|
|
|
@@ -157,12 +159,35 @@ export function mergeProfile(existing, plan, previousRecord) {
|
|
|
157
159
|
return `---\n${front.join('\n')}\n---\n${body.startsWith('\n') ? '' : '\n'}${body}`;
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The profile is one person's dials (knowledge-sync never sends it), so it never belongs in a commit
|
|
164
|
+
* either — yet it sat untracked for good, and every session's agent reported it as «cambios sin
|
|
165
|
+
* commitear» (E2E 2026-10-07). Excluded in THIS clone only (`info/exclude`, not `.gitignore`), and
|
|
166
|
+
* never when the team already tracks it: that is their decision.
|
|
167
|
+
*/
|
|
168
|
+
export function excludePersonalProfile(cwd) {
|
|
169
|
+
const git = (args) => { try { return execFileSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; } };
|
|
170
|
+
if (git(['rev-parse', '--is-inside-work-tree']) !== 'true') return false;
|
|
171
|
+
const rel = '02-DOCS/wiki/harness/user-profile.md';
|
|
172
|
+
if (git(['ls-files', '--', rel])) return false;
|
|
173
|
+
const value = git(['rev-parse', '--git-path', 'info/exclude']);
|
|
174
|
+
if (!value) return false;
|
|
175
|
+
const file = resolve(cwd, value);
|
|
176
|
+
const pattern = `/${rel}`;
|
|
177
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
178
|
+
const body = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
179
|
+
if (body.split('\n').includes(pattern)) return true;
|
|
180
|
+
appendFileSync(file, `${body && !body.endsWith('\n') ? '\n' : ''}${pattern}\n`);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
160
184
|
export function writeOnboardingDocuments(cwd, plan, planId, previousRecord = null) {
|
|
161
185
|
const dir = join(cwd, '02-DOCS', 'wiki', 'harness');
|
|
162
186
|
mkdirSync(dir, { recursive: true });
|
|
163
187
|
const docs = renderOnboardingDocuments(plan, planId);
|
|
164
188
|
const profilePath = join(dir, 'user-profile.md');
|
|
165
189
|
writeFileSync(profilePath, mergeProfile(existsSync(profilePath) ? readFileSync(profilePath, 'utf8') : '', plan, previousRecord));
|
|
190
|
+
excludePersonalProfile(cwd);
|
|
166
191
|
writeFileSync(join(dir, 'installation-plan.md'), docs.installation);
|
|
167
192
|
const decisionsPath = join(dir, 'decisions.md');
|
|
168
193
|
if (!existsSync(decisionsPath)) writeFileSync(decisionsPath, docs.decisions);
|
|
@@ -343,34 +368,15 @@ export async function applyAcceptedOnboarding({ cwd = process.cwd(), plan, planI
|
|
|
343
368
|
// Y la retro-compatibilidad no es una lista de exentos (P3): el suelo viaja DENTRO del plan, así
|
|
344
369
|
// que un recibo aceptado por una versión anterior no lo trae y queda exento por construcción.
|
|
345
370
|
|
|
346
|
-
const LIB_DIR = dirname(fileURLToPath(import.meta.url));
|
|
347
|
-
const TEMPLATE_SOURCE = join(LIB_DIR, '..', '..', 'skills', 'harness', 'assets', '_TEMPLATE');
|
|
348
371
|
const TEMPLATE_FLOOR = '01-TOOLS/_TEMPLATE/';
|
|
349
372
|
|
|
350
373
|
export const HARNESS_FLOOR_MINIMUM = [TEMPLATE_FLOOR, '02-DOCS/wiki/harness/'];
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// añadir un fichero a la plantilla no deje el suelo comprobando de menos en silencio.
|
|
355
|
-
//
|
|
356
|
-
// Un error aquí NO se convierte en lista vacía. Comérselo dejaba el suelo permanentemente
|
|
357
|
-
// insatisfacible, con un mensaje que ofrecía una acción que leía ese mismo asset ausente y que por
|
|
358
|
-
// tanto no podía arreglarlo nunca — y sin nombrar jamás la causa real, que es un paquete roto.
|
|
359
|
-
function templateAssets() {
|
|
360
|
-
return readdirSync(TEMPLATE_SOURCE).sort();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// npm NUNCA empaqueta un fichero llamado `.gitignore`, así que el asset viaja sin punto y se copia
|
|
364
|
-
// con él. Sin esto, desde el paquete publicado se copiaban 4 de 5 ficheros y el que faltaba era el
|
|
365
|
-
// único que evita comitear el `.env` que el README —copiado por el propio instalador— manda crear.
|
|
366
|
-
const DOTTED = { gitignore: '.gitignore' };
|
|
367
|
-
const targetName = (asset) => DOTTED[asset] ?? asset;
|
|
374
|
+
// El onboarding escribe ahora un BORRADOR (`constitution-draft.js`) y el suelo lo acepta: sólo
|
|
375
|
+
// mira existencia, y completar el borrador es de la cadena SDD, no del instalador.
|
|
376
|
+
export const HARNESS_FLOOR_CONSTITUTION = CONSTITUTION_PATH;
|
|
368
377
|
|
|
369
|
-
// La
|
|
370
|
-
//
|
|
371
|
-
// de que nadie se salte un paso con credenciales de por medio.
|
|
372
|
-
const LAYER_IGNORE = ['# Escrito por rsc: la capa de herramientas guarda credenciales.',
|
|
373
|
-
'*/.env', '*/.env.*', '!*/.env.example', '*/keys/', '*/out/', ''].join('\n');
|
|
378
|
+
// La plantilla, sus nombres con punto y el `.gitignore` de la capa viven en `tools-skeleton.js`:
|
|
379
|
+
// lo que se copia aquí es exactamente lo que `purge` puede recuperar, y sólo si sigue intacto.
|
|
374
380
|
|
|
375
381
|
/**
|
|
376
382
|
* Crea la parte determinista del esqueleto. Copiar ficheros estáticos es un algoritmo, así que por
|
|
@@ -117,6 +117,13 @@ export function scanProject(root = process.cwd()) {
|
|
|
117
117
|
// Codex stores the always-on layer in AGENTS.md. Ignore a file that contains
|
|
118
118
|
// only our managed block, while retaining a user's surrounding instructions
|
|
119
119
|
// as real project evidence across repair/re-onboarding.
|
|
120
|
+
// El `CLAUDE.md` raíz es el Knowledge map, y lo escriben las propias fases de rsc: la
|
|
121
|
+
// `constitution` añade su fila «create CLAUDE.md if absent», y el instalador puede dejar ahí
|
|
122
|
+
// la sombra de AGENTS.md. Contarlo como evidencia movía la identidad del plan justo después de
|
|
123
|
+
// completar el suelo, y el `--accept-plan` del mismo id moría con RSC_PLAN_CHANGED sin que
|
|
124
|
+
// nada se hubiera elegido distinto (reproducido con 3.0.8). La cuenta de markdown no decide
|
|
125
|
+
// nada del plan —sólo su identidad—, así que no se pierde ninguna decisión.
|
|
126
|
+
if (rel === 'CLAUDE.md') isSignal = false;
|
|
120
127
|
if (entry.name === 'AGENTS.md') {
|
|
121
128
|
const human = readFileSync(path, 'utf8')
|
|
122
129
|
.replace(/\n*<!-- rsc-suggest:start -->[\s\S]*?<!-- rsc-suggest:end -->\n*/g, '')
|
|
@@ -248,7 +255,14 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
248
255
|
// in an invoicing workspace, and `init` promises it by technical level: non-technical and mixed get
|
|
249
256
|
// it, whatever the project is. The guard also decides at runtime from the profile, so the plan only
|
|
250
257
|
// chooses whether it is present, never whether it bites.
|
|
251
|
-
|
|
258
|
+
//
|
|
259
|
+
// And since 3.0.8 it is present for EVERYONE. A technical user used to get nothing, so a small
|
|
260
|
+
// project with `main` open had no guard at all (E2E 2026-10-07). The runtime still decides the
|
|
261
|
+
// bite: deny for non-technical, `ask` — only on lost work or rewritten history — for technical.
|
|
262
|
+
const dangerGuard = true;
|
|
263
|
+
// The FTD/SDD lane decision belongs to every software project, small ones included: without it
|
|
264
|
+
// the agent never sees the lanes (E2E 2026-10-07: five sessions, zero feature documents).
|
|
265
|
+
const laneGate = isSoftware;
|
|
252
266
|
// `sdd` is installed everywhere now, but its DECISION is about practice, not presence — so where
|
|
253
267
|
// the chain is not practised the explicit `deferred` entry below must be the only one, or the
|
|
254
268
|
// generic "installed → selected" mapping would shadow it and `reassess` would see nothing to watch.
|
|
@@ -269,9 +283,16 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
269
283
|
// so the chain is always one request away; this decision is what `reassess` watches, and what the
|
|
270
284
|
// floor reads. Collapsing the two would have deleted reassess's reason to exist as a side effect
|
|
271
285
|
// of an installer change, which is not a decision an installer change gets to make.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
// The reason says both halves, because the plan lists `.claude/skills/sdd` under managed paths
|
|
287
|
+
// and a bare "deferred" next to it read as a contradiction (E2E 2026-10-07). Only the wording
|
|
288
|
+
// changes: the decision stays the one deferred `workflow/sdd` entry that reassess watches.
|
|
289
|
+
const whyDeferred = isSoftware
|
|
290
|
+
? 'the software scope is small, so specification overhead is not justified yet.'
|
|
291
|
+
: 'SDD applies to substantial software work, which is not the declared project purpose.';
|
|
292
|
+
const sddReason = skills.includes('sdd')
|
|
293
|
+
? `The sdd skill is installed, so the chain is one request away; the practice is deferred: ${whyDeferred}`
|
|
294
|
+
: whyDeferred.charAt(0).toUpperCase() + whyDeferred.slice(1);
|
|
295
|
+
if (!practisesSdd) decisions.push(deferred('sdd', 'workflow', sddReason, sddTriggers, softwareTriggers));
|
|
275
296
|
if (baseAgents) {
|
|
276
297
|
for (const id of agents) decisions.push(selected(id, 'agent', 'The accepted substantial software workflow requires this implementation or review role.'));
|
|
277
298
|
} else {
|
|
@@ -296,6 +317,7 @@ export function buildOnboardingPlan(record, evidence) {
|
|
|
296
317
|
codeHooks: practisesSdd,
|
|
297
318
|
gitmojiGuard,
|
|
298
319
|
dangerGuard,
|
|
320
|
+
laneGate,
|
|
299
321
|
memory: true,
|
|
300
322
|
context7: false,
|
|
301
323
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
// The `01-TOOLS/` scaffolding rsc writes on onboarding, in ONE place: what `ensureHarnessSkeleton`
|
|
7
|
+
// copies is exactly what `purge` may take back, and it may take back only a file still identical to
|
|
8
|
+
// what was copied. `01-TOOLS/` is the layer the user edits when adding tools, so anything that
|
|
9
|
+
// differs from the template is theirs and stays.
|
|
10
|
+
|
|
11
|
+
const LIB_DIR = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
export const TEMPLATE_SOURCE = join(LIB_DIR, '..', '..', 'skills', 'harness', 'assets', '_TEMPLATE');
|
|
13
|
+
|
|
14
|
+
// Los ficheros que la plantilla trae hoy. Se lee del asset en vez de codificar una lista, para que
|
|
15
|
+
// añadir un fichero a la plantilla no deje el suelo comprobando de menos en silencio.
|
|
16
|
+
//
|
|
17
|
+
// Un error aquí NO se convierte en lista vacía. Comérselo dejaba el suelo permanentemente
|
|
18
|
+
// insatisfacible, con un mensaje que ofrecía una acción que leía ese mismo asset ausente y que por
|
|
19
|
+
// tanto no podía arreglarlo nunca — y sin nombrar jamás la causa real, que es un paquete roto.
|
|
20
|
+
export function templateAssets() {
|
|
21
|
+
return readdirSync(TEMPLATE_SOURCE).sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// npm NUNCA empaqueta un fichero llamado `.gitignore`, así que el asset viaja sin punto y se copia
|
|
25
|
+
// con él. Sin esto, desde el paquete publicado se copiaban 4 de 5 ficheros y el que faltaba era el
|
|
26
|
+
// único que evita comitear el `.env` que el README —copiado por el propio instalador— manda crear.
|
|
27
|
+
const DOTTED = { gitignore: '.gitignore' };
|
|
28
|
+
export const targetName = (asset) => DOTTED[asset] ?? asset;
|
|
29
|
+
|
|
30
|
+
// La capa entera, no cada proveedor: el flujo documentado es copiar `_TEMPLATE/` a
|
|
31
|
+
// `01-TOOLS/<PROVEEDOR>/`, y depender de que cada copia se lleve su propia protección es depender
|
|
32
|
+
// de que nadie se salte un paso con credenciales de por medio.
|
|
33
|
+
export const LAYER_IGNORE = ['# Escrito por rsc: la capa de herramientas guarda credenciales.',
|
|
34
|
+
'*/.env', '*/.env.*', '!*/.env.example', '*/keys/', '*/out/', ''].join('\n');
|
|
35
|
+
|
|
36
|
+
const sameBytes = (a, b) => {
|
|
37
|
+
try { return readFileSync(a).equals(Buffer.isBuffer(b) ? b : readFileSync(b)); } catch { return false; }
|
|
38
|
+
};
|
|
39
|
+
const isPlainFile = (p) => { try { return lstatSync(p).isFile(); } catch { return false; } };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Take back the scaffolding rsc wrote, file by file, only where it is still byte-identical to what
|
|
43
|
+
* rsc wrote. Returns `{ removed, kept }` as paths relative to `cwd`, `kept` with a reason. Empty
|
|
44
|
+
* directories left behind are removed by the caller's directory sweep.
|
|
45
|
+
*/
|
|
46
|
+
// Committed means the team has it: deleting it leaves the tree dirty and takes it from everybody on
|
|
47
|
+
// the next push (field test 3.0.8: purge deleted a 01-TOOLS/.gitignore that was already in history).
|
|
48
|
+
function tracked(cwd, rel) {
|
|
49
|
+
try { return Boolean(execFileSync('git', ['ls-files', '--', rel], { cwd, encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim()); } catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function removeUnmodifiedSkeleton(cwd, { dryRun = false } = {}) {
|
|
53
|
+
const removed = [];
|
|
54
|
+
const kept = [];
|
|
55
|
+
const committed = (rel) => tracked(cwd, rel) && (kept.push({ path: rel, reason: 'committed to git — the team has it; delete it in a commit if you mean to' }), true);
|
|
56
|
+
const templateDir = join(cwd, '01-TOOLS', '_TEMPLATE');
|
|
57
|
+
if (existsSync(templateDir) && !lstatSync(templateDir).isSymbolicLink()) {
|
|
58
|
+
for (const asset of templateAssets()) {
|
|
59
|
+
const rel = `01-TOOLS/_TEMPLATE/${targetName(asset)}`;
|
|
60
|
+
const file = join(cwd, rel);
|
|
61
|
+
if (!existsSync(file) || committed(rel)) continue;
|
|
62
|
+
if (isPlainFile(file) && sameBytes(file, join(TEMPLATE_SOURCE, asset))) {
|
|
63
|
+
if (!dryRun) rmSync(file, { force: true });
|
|
64
|
+
removed.push(rel);
|
|
65
|
+
} else kept.push({ path: rel, reason: 'changed since rsc wrote it — yours now' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const layer = join(cwd, '01-TOOLS', '.gitignore');
|
|
69
|
+
if (existsSync(layer) && !committed('01-TOOLS/.gitignore')) {
|
|
70
|
+
if (isPlainFile(layer) && sameBytes(layer, Buffer.from(LAYER_IGNORE))) {
|
|
71
|
+
if (!dryRun) rmSync(layer, { force: true });
|
|
72
|
+
removed.push('01-TOOLS/.gitignore');
|
|
73
|
+
} else kept.push({ path: '01-TOOLS/.gitignore', reason: 'changed since rsc wrote it — yours now' });
|
|
74
|
+
}
|
|
75
|
+
return { removed, kept };
|
|
76
|
+
}
|
package/scripts/rsc.js
CHANGED
|
@@ -4,12 +4,13 @@ import { detectTarget, installedTargets, resolveTargets, TARGETS } from '../targ
|
|
|
4
4
|
import { detectRepo } from './detect-repo.js';
|
|
5
5
|
import { rank } from './consult.js';
|
|
6
6
|
import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
|
|
7
|
-
import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled,
|
|
7
|
+
import { applyInstall, listInstalled, listInstalledAgents, listInstalledCommands, uninstall, syncInstalled, purgeReport, collisions, ownSkillPaths } from './install-apply.js';
|
|
8
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
8
9
|
import { stackAgentNames } from '../targets/agents.js';
|
|
9
10
|
import { doctor } from './doctor.js';
|
|
10
11
|
import { ask, say, select, pickFrom, banner, confirm, isInteractive } from './lib/ui.js';
|
|
11
12
|
import { refreshRegistry, registryStatus } from './lib/registry.js';
|
|
12
|
-
import { audit, writeAuditReport } from './audit.js';
|
|
13
|
+
import { audit, writeAuditReport, recordAuditBaseline } from './audit.js';
|
|
13
14
|
import { DOMAINS } from './lib/domains.js';
|
|
14
15
|
import { listBackups, restoreBackup, whereKept } from './lib/backups.js';
|
|
15
16
|
import { runUpgrade } from './lib/upgrade.js';
|
|
@@ -18,11 +19,13 @@ import { DEFAULT_SKILL_FLOOR, withDefaultSkillFloor } from './lib/default-skill-
|
|
|
18
19
|
import { RETIRED_SKILLS, replaceRetired } from './lib/retired-skills.js';
|
|
19
20
|
import { readManifest, writeManifest } from './lib/manifest-file.js';
|
|
20
21
|
import { versionReport } from './lib/versions.js';
|
|
22
|
+
import { templateAssets, targetName } from './lib/tools-skeleton.js';
|
|
21
23
|
import {
|
|
22
24
|
normalizeOnboarding, missingOnboardingFields, scanProject,
|
|
23
25
|
buildOnboardingPlan, decodeGoal, encodeGoal, identifyPlan, recommendDeferredComponents,
|
|
24
26
|
} from './lib/onboarding.js';
|
|
25
27
|
import { applyAcceptedOnboarding, verifyOnboarding, ensureHarnessSkeleton, harnessReadiness } from './lib/onboarding-apply.js';
|
|
28
|
+
import { CONSTITUTION_PATH, constitutionIsDraft, ensureConstitutionDraft } from './lib/constitution-draft.js';
|
|
26
29
|
|
|
27
30
|
const rawArgv = process.argv.slice(2);
|
|
28
31
|
|
|
@@ -39,6 +42,33 @@ if (['--version', '-v', 'version'].includes(rawArgv[0])) {
|
|
|
39
42
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
40
43
|
process.exit(0);
|
|
41
44
|
}
|
|
45
|
+
const GENERAL_HELP = [
|
|
46
|
+
'Use: npx @ericrisco/rsc onboard | reassess | add <id...> | install --profile <p> | consult "<text>" | list | capabilities [--full|gap-log] | audit | registry refresh | doctor | sync | memory <on|off|status|save|resume|learn|metrics> | sello <on|off|status|…> | worktrees [reap [path] [--confirm]] | backups | restore <id|latest> | upgrade | repair | uninstall <id...> | purge | help',
|
|
47
|
+
'Any command takes --target <claude|codex|cursor|copilot|gemini|…> (comma-separate for several)',
|
|
48
|
+
' → without it, rsc uses the assistant already installed here; if two are, it asks instead of guessing.',
|
|
49
|
+
].join('\n');
|
|
50
|
+
const UNINSTALL_HELP = [
|
|
51
|
+
'Use: npx @ericrisco/rsc uninstall <id...> [--dry-run] [--target <assistant>]',
|
|
52
|
+
' Removes the named skills or agents from this project.',
|
|
53
|
+
' To remove everything rsc installed here, run: npx @ericrisco/rsc purge (keeps 02-DOCS/ unless --with-docs)',
|
|
54
|
+
].join('\n');
|
|
55
|
+
const PURGE_HELP = [
|
|
56
|
+
'Use: npx @ericrisco/rsc purge [--dry-run] [--with-docs]',
|
|
57
|
+
' Removes everything rsc installed in this project and reports what was removed, cleaned and kept.',
|
|
58
|
+
' --dry-run shows the report without touching anything. 02-DOCS/ is kept unless --with-docs.',
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
// Asking for help must never RUN the command. `uninstall --help` used to be read as an empty id list
|
|
62
|
+
// and report "Removed: (nothing)"; `purge --help` would have purged. Answered before anything else.
|
|
63
|
+
{
|
|
64
|
+
const helpAsked = rawArgv.some((a) => a === '--help' || a === '-h');
|
|
65
|
+
if (rawArgv[0] === 'help' || helpAsked) {
|
|
66
|
+
const about = rawArgv[0] === 'help' ? rawArgv[1] : rawArgv.find((a) => !a.startsWith('-'));
|
|
67
|
+
const text = about === 'uninstall' ? UNINSTALL_HELP : about === 'purge' ? PURGE_HELP : GENERAL_HELP;
|
|
68
|
+
process.stdout.write(`${text}\n`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
42
72
|
// `--accompaniment` stays listed although the dial is retired: old scripts and agents still pass it,
|
|
43
73
|
// and its value must keep being skipped rather than read as a command or a skill id.
|
|
44
74
|
const GLOBAL_VALUE_FLAGS = new Set([
|
|
@@ -159,6 +189,12 @@ function renderPlan(plan, planId) {
|
|
|
159
189
|
for (const decision of plan.decisions.filter((d) => d.state === 'excluded')) say(` × ${decision.kind}/${decision.id} — ${decision.reason}`);
|
|
160
190
|
say('Managed paths:');
|
|
161
191
|
for (const path of plan.governedPaths) say(` ${path}`);
|
|
192
|
+
// The scaffolding is written once and then belongs to the person, so it is not a managed (digested)
|
|
193
|
+
// path — but it IS written by this plan, and a plan that does not show it is not the whole plan.
|
|
194
|
+
if ((plan.floorPaths || []).includes('01-TOOLS/_TEMPLATE/')) {
|
|
195
|
+
say('Scaffolded once (yours to edit; never overwritten; purge removes only untouched files):');
|
|
196
|
+
for (const path of ['01-TOOLS/.gitignore', ...templateAssets().map((a) => `01-TOOLS/_TEMPLATE/${targetName(a)}`)].sort()) say(` ${path}`);
|
|
197
|
+
}
|
|
162
198
|
if (plan.replacesUserSkills?.length) {
|
|
163
199
|
say('Your own skills this plan replaces:');
|
|
164
200
|
for (const path of plan.replacesUserSkills) say(` ! ${path} — already here and not put by rsc; the catalog skill of the same name takes its place, and yours is kept in .rsc/backups/`);
|
|
@@ -166,8 +202,11 @@ function renderPlan(plan, planId) {
|
|
|
166
202
|
say(' AGENT: name each of these to the user before accepting, and ask whether to replace or rename.');
|
|
167
203
|
}
|
|
168
204
|
if (plan.evidence.parentHarness) say(`Parent harness detected at ${plan.evidence.parentHarness}; it is not inherited by this plan.`);
|
|
205
|
+
// Pinned to the version that computed this plan. The id is a hash of the plan, and the plan
|
|
206
|
+
// depends on the catalog of the running version: `@latest` would accept it with whatever version
|
|
207
|
+
// npm resolves next, which answers RSC_PLAN_CHANGED the day a release lands in between.
|
|
169
208
|
const pieces = [
|
|
170
|
-
|
|
209
|
+
`npx @ericrisco/rsc@${versionReport(process.cwd()).cli} onboard`,
|
|
171
210
|
`--technical-level ${plan.record.technicalLevel}`,
|
|
172
211
|
`--project-kind ${plan.record.projectKind}`,
|
|
173
212
|
`--goal-base64 ${encodeGoal(plan.record.goal)}`,
|
|
@@ -260,6 +299,17 @@ async function runOnboarding(targets) {
|
|
|
260
299
|
} catch (error) {
|
|
261
300
|
say(`RSC_SKELETON_FAILED ${error.message}`);
|
|
262
301
|
}
|
|
302
|
+
// La constitución, como BORRADOR, cuando el plan eligió SDD y no hay ninguna. Sin esto una
|
|
303
|
+
// instalación no interactiva de software complejo acababa INCOMPLETE: sólo una fase agéntica la
|
|
304
|
+
// escribía. Mismo sitio y mismo motivo que el esqueleto: fuera de la transacción, sin residuo.
|
|
305
|
+
try {
|
|
306
|
+
ensureConstitutionDraft(process.cwd(), plan);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
say(`RSC_SKELETON_FAILED ${error.message}`);
|
|
309
|
+
}
|
|
310
|
+
// The audit cadence starts at install. Without a baseline the very first session after onboarding
|
|
311
|
+
// announced "a skill audit is due" about a set chosen minutes earlier. Never overwrites a real run.
|
|
312
|
+
try { recordAuditBaseline(process.cwd()); } catch { /* a missing baseline only means an early nudge */ }
|
|
263
313
|
for (const path of plan.replacesUserSkills || []) {
|
|
264
314
|
const kept = whereKept({ cwd: process.cwd(), relPath: path });
|
|
265
315
|
if (kept) say(`Your previous ${path} is kept in ${kept}`);
|
|
@@ -272,7 +322,14 @@ async function runOnboarding(targets) {
|
|
|
272
322
|
// poco se cumplía entero. Spec: 02-DOCS/wiki/sdd/specs/install-completion-floor.md
|
|
273
323
|
function emitHarnessReadiness(plan, planId) {
|
|
274
324
|
const readiness = harnessReadiness(process.cwd(), plan);
|
|
275
|
-
if (readiness.ready)
|
|
325
|
+
if (readiness.ready) {
|
|
326
|
+
say(`RSC_ONBOARDING_READY ${planId}`);
|
|
327
|
+
// Listo no es «nada pendiente»: el borrador se nombra para que la cadena SDD lo complete primero.
|
|
328
|
+
if ((plan.floorPaths || []).includes(CONSTITUTION_PATH) && constitutionIsDraft(process.cwd())) {
|
|
329
|
+
say(` Pending: ${CONSTITUTION_PATH} is a draft — complete it with the \`constitution\` phase before the first SDD feature.`);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
276
333
|
say(`RSC_ONBOARDING_INCOMPLETE ${planId}`);
|
|
277
334
|
for (const missing of safeLines(readiness.missing)) say(` ${missing}`);
|
|
278
335
|
say(` Next: ${readiness.action}`);
|
|
@@ -370,10 +427,34 @@ function flag(name) {
|
|
|
370
427
|
// Remove everything rsc installed in this project (skills, hooks, .rsc/), across
|
|
371
428
|
// every assistant. Keeps 02-DOCS/ unless --with-docs. `purge` / `uninstall --all`.
|
|
372
429
|
async function runPurge(dryRun, withDocs) {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
430
|
+
const cwd = process.cwd();
|
|
431
|
+
const report = await purgeReport({ cwd, withDocs, dryRun });
|
|
432
|
+
// Relative to the project: the report is read by a person, and every path is inside it.
|
|
433
|
+
const shown = (p) => {
|
|
434
|
+
const rel = relative(cwd, p);
|
|
435
|
+
return !rel || rel.startsWith('..') || isAbsolute(rel) ? p : rel.split(sep).join('/');
|
|
436
|
+
};
|
|
437
|
+
say(`${dryRun ? 'Would remove' : 'Removed'} ${report.removed.length} path(s):`);
|
|
438
|
+
for (const r of report.removed.map(shown).sort()) say(` - ${r}`);
|
|
439
|
+
if (report.cleaned.length) {
|
|
440
|
+
say(`\n${dryRun ? 'Would clean' : 'Cleaned'} ${report.cleaned.length} file(s) — rsc's entries ${dryRun ? 'would be' : 'were'} taken out; ${dryRun ? 'each is deleted if nothing of yours remains' : 'the rest of the file is yours and stays'}:`);
|
|
441
|
+
for (const c of report.cleaned.map(shown).sort()) say(` - ${c}`);
|
|
442
|
+
}
|
|
443
|
+
if (report.kept.length) {
|
|
444
|
+
say(`\nKept ${report.kept.length}:`);
|
|
445
|
+
for (const k of report.kept) say(` - ${k.path} — ${k.reason}`);
|
|
446
|
+
}
|
|
447
|
+
// On a clone most of what purge deletes is committed. Deleting it here is local; committing the
|
|
448
|
+
// deletions uninstalls rsc for the whole team (team simulation D15). Said before anyone commits.
|
|
449
|
+
if (report.tracked?.length) {
|
|
450
|
+
const quote = (p) => (/^[\w./@+-]+$/.test(p) ? p : `'${p.replace(/'/g, `'\\''`)}'`);
|
|
451
|
+
say(`\nWARNING: ${report.tracked.length} of these file(s) are committed in git and ${dryRun ? 'would show' : 'now show'} as deleted or changed:`);
|
|
452
|
+
for (const f of report.tracked.slice(0, 12)) say(` - ${f}`);
|
|
453
|
+
if (report.tracked.length > 12) say(` … and ${report.tracked.length - 12} more`);
|
|
454
|
+
say('Committing these changes uninstalls rsc for the whole team, on their next pull. To remove rsc only on this machine, do not commit them.');
|
|
455
|
+
if (!dryRun) say(`To undo locally: git restore -- ${report.tracked.map(quote).join(' ')}`);
|
|
456
|
+
else say('To undo after a real purge: git restore -- <those files> (purge prints the exact command).');
|
|
457
|
+
}
|
|
377
458
|
}
|
|
378
459
|
|
|
379
460
|
async function recommendIds(query, { labeledOnly = false } = {}) {
|
|
@@ -515,7 +596,7 @@ function printContextBudget(b) {
|
|
|
515
596
|
if (b.notApplicable.length) {
|
|
516
597
|
say(` ${b.note}`);
|
|
517
598
|
} else {
|
|
518
|
-
say(` Per session start : ${kb(b.sessionStartBytes)} (always-on body, per wired scope)`);
|
|
599
|
+
say(` Per session start : ${kb(b.sessionStartBytes)} (always-on body, per wired scope; one-time and conditional banners not counted)`);
|
|
519
600
|
say(` Per user turn : ${kb(b.perTurnBytes)}`);
|
|
520
601
|
}
|
|
521
602
|
say(` Skill descriptions: ${kb(b.descriptionsBytes)} (${b.installedSkills} installed, always in context)`);
|
|
@@ -1006,7 +1087,15 @@ async function main() {
|
|
|
1006
1087
|
// defeated by an argument the sweep itself told the agent to type.
|
|
1007
1088
|
const confirmed = argv.includes('--confirm');
|
|
1008
1089
|
const targets = one ? [one] : candidates.filter((c) => c.verdict === 'safe').map((c) => c.path);
|
|
1009
|
-
|
|
1090
|
+
// The bulk reap also deletes plain local branches that have landed (`branch -d`, never -D):
|
|
1091
|
+
// the worktree is not the only thing a merge leaves behind (E2E defect 14).
|
|
1092
|
+
const reapBranches = () => {
|
|
1093
|
+
if (one) return 0;
|
|
1094
|
+
const { deleted } = W.reapMergedBranches(root);
|
|
1095
|
+
for (const b of deleted) say(`✅ deleted merged branch ${b}`);
|
|
1096
|
+
return deleted.length;
|
|
1097
|
+
};
|
|
1098
|
+
if (!targets.length) { if (!reapBranches()) say('nothing to clean up.'); return; }
|
|
1010
1099
|
for (const t of targets) {
|
|
1011
1100
|
const out = W.reapWorktree(root, t, { confirmed });
|
|
1012
1101
|
if (out.removed) {
|
|
@@ -1018,6 +1107,7 @@ async function main() {
|
|
|
1018
1107
|
process.exitCode = 1;
|
|
1019
1108
|
}
|
|
1020
1109
|
}
|
|
1110
|
+
reapBranches();
|
|
1021
1111
|
return;
|
|
1022
1112
|
}
|
|
1023
1113
|
|
|
@@ -1272,7 +1362,14 @@ async function main() {
|
|
|
1272
1362
|
const dry = argv.includes('--dry-run');
|
|
1273
1363
|
// `uninstall --all` is an alias for a full purge.
|
|
1274
1364
|
if (argv.includes('--all')) return void (await runPurge(dry, argv.includes('--with-docs')));
|
|
1275
|
-
const
|
|
1365
|
+
const ids = requestedIds();
|
|
1366
|
+
// No id is not "nothing to remove": it is a command that was not given what it needs.
|
|
1367
|
+
if (!ids.length) {
|
|
1368
|
+
console.error(`rsc: uninstall needs at least one skill or agent id.\n${UNINSTALL_HELP}`);
|
|
1369
|
+
process.exitCode = 1;
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const selected = classifyRequested(ids);
|
|
1276
1373
|
// A retired skill an old install still holds is not unknown: the state file proves it is ours.
|
|
1277
1374
|
const held = new Set(listInstalled({ target }));
|
|
1278
1375
|
selected.skills.push(...selected.unknown.filter((id) => Object.hasOwn(RETIRED_SKILLS, id) && held.has(id)));
|
|
@@ -1306,9 +1403,7 @@ async function main() {
|
|
|
1306
1403
|
return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
|
|
1307
1404
|
default:
|
|
1308
1405
|
say(`rsc: unknown command '${cmd}'.`);
|
|
1309
|
-
say(
|
|
1310
|
-
say('Any command takes --target <claude|codex|cursor|copilot|gemini|…> (comma-separate for several)');
|
|
1311
|
-
say(' → without it, rsc uses the assistant already installed here; if two are, it asks instead of guessing.');
|
|
1406
|
+
say(GENERAL_HELP);
|
|
1312
1407
|
process.exitCode = 1;
|
|
1313
1408
|
}
|
|
1314
1409
|
}
|
|
@@ -86,6 +86,8 @@ Write `02-DOCS/wiki/sdd/constitution.md` from the template in `references/consti
|
|
|
86
86
|
|
|
87
87
|
Create `02-DOCS/wiki/sdd/` if it does not exist. Do not overwrite an existing constitution — amend it.
|
|
88
88
|
|
|
89
|
+
**A draft is "complete me", not "amend me".** `rsc onboard` writes a skeleton with `status: draft` in the frontmatter: the template's headings, the facts onboarding knew (goal, kind, scope, workflow, detected stack) and no principle. Keep those facts, run the interview, fill the sections, then on ratification drop `status: draft`, set `version: v1.0.0` and log v1.0.0 in the amendment log.
|
|
90
|
+
|
|
89
91
|
## Versioning & amendment protocol
|
|
90
92
|
|
|
91
93
|
The constitution is versioned so `analyze` and `review` can cite "constitution v1.2.0, principle 4".
|
package/skills/ftd/SKILL.md
CHANGED
|
@@ -34,6 +34,11 @@ repository**: whoever clones it should get code, not somebody else's notes in pr
|
|
|
34
34
|
It is written **before the first change**, not after, and it holds exactly this:
|
|
35
35
|
|
|
36
36
|
```markdown
|
|
37
|
+
---
|
|
38
|
+
author: <who asked for it — git user.name>
|
|
39
|
+
branch: <the branch the work runs on>
|
|
40
|
+
status: in-progress
|
|
41
|
+
---
|
|
37
42
|
# <feature>
|
|
38
43
|
|
|
39
44
|
## Intent — what changes, and why it is worth doing
|
|
@@ -43,6 +48,10 @@ It is written **before the first change**, not after, and it holds exactly this:
|
|
|
43
48
|
## Next — the single next step, so an interruption costs nothing
|
|
44
49
|
```
|
|
45
50
|
|
|
51
|
+
The frontmatter says whose work it is, so «¿qué ha hecho cada uno?» has an answer. You never close
|
|
52
|
+
it by hand: when the branch lands on the default branch, rsc's post-merge hook sets `status: done`
|
|
53
|
+
and replaces Next with where and when it merged (on a closed default branch, via `rsc/knowledge`).
|
|
54
|
+
|
|
46
55
|
No decision journal, no phase records, no approvals. If a decision was meaningful, one line of
|
|
47
56
|
rationale goes next to the task it belongs to.
|
|
48
57
|
|