@trycore/spec-build-harness 0.10.0 → 0.11.1
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/.claude-plugin/plugin.json +1 -1
- package/VERSION +1 -1
- package/commands/build/escalate.md +1 -1
- package/commands/build/onboard.md +20 -4
- package/commands/build/release.md +4 -3
- package/dist/cli.js +11 -1
- package/dist/commands/migrate.js +107 -2
- package/dist/lib/normalize.js +925 -78
- package/dist/lib/state-bundle.js +112 -8
- package/docs/runtime/guia-modo-dual-y-migracion.md +9 -2
- package/docs/runtime/protocolo-cliente-runtime.md +29 -19
- package/hooks/build/event-emitter.sh +9 -52
- package/hooks/build/lib/runtime-client.sh +183 -23
- package/hooks/build/lib/runtime-ops.sh +14 -8
- package/hooks/build/lib/state-io.sh +0 -0
- package/hooks/build/release-ops.sh +48 -16
- package/hooks/build/slice-ops.sh +152 -81
- package/package.json +1 -1
- package/scripts/check-runtime-purity.sh +0 -0
- package/scripts/denylist.txt +4 -0
- package/scripts/lib/graph-bundle.py +68 -20
- package/scripts/tests/test-baseline-verdict.sh +0 -0
- package/scripts/tests/test-hooks-runtime.sh +13 -38
- package/scripts/tests/test-install.sh +46 -0
- package/scripts/tests/test-skill-ops.sh +578 -58
- package/skills/building-a-slice/references/runtime-protocol.md +1 -1
- package/skills/releasing-a-version/SKILL.md +10 -6
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "trycore-spec-build-harness",
|
|
4
4
|
"displayName": "Trycore — Spec & Build Harness",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.11.1",
|
|
6
6
|
"description": "Arnés de construcción de dos loops (slice por épica + release gate) para Claude Code, con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Trycore",
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.11.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "BUILD: Escalate"
|
|
3
|
-
description: Registra un bloqueo del slice en el runtime (evento
|
|
3
|
+
description: Registra un bloqueo del slice en el runtime (evento slice_escalated) y devuelve la decisión al humano — recortar, diferir o desbloquear nunca lo decide el modelo. Adaptador delgado sobre slice-ops.sh escalate.
|
|
4
4
|
category: Workflow
|
|
5
5
|
tags: [build-harness, runtime, escalada, gobierno, trycore]
|
|
6
6
|
---
|
|
@@ -253,18 +253,34 @@ el token de agente. El arnés **prepara y valida**; una persona sube.
|
|
|
253
253
|
"release_lines": [{"id": "R1-mvp", "epics": ["EP-001"]}]}
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
2. Normalízalo y valídalo (determinista, nunca sube nada)
|
|
256
|
+
2. Normalízalo y valídalo (determinista, nunca sube nada). La salida va a
|
|
257
|
+
`.claude/state/graph-bundle.json` para que `trycore-build migrate` la encuentre sola:
|
|
257
258
|
|
|
258
259
|
```bash
|
|
259
|
-
python3 .claude/scripts/lib/graph-bundle.py < /tmp/epics.json > /
|
|
260
|
+
python3 .claude/scripts/lib/graph-bundle.py < /tmp/epics.json > .claude/state/graph-bundle.json
|
|
260
261
|
```
|
|
261
262
|
|
|
263
|
+
El script emite las épicas en el **formato exacto del hub**: `layer` en MAYÚSCULA
|
|
264
|
+
(`FOUNDATIONAL`|`BUSINESS` — un layer inválido rechaza el grafo ENTERO), historias con
|
|
265
|
+
`code`, y `release_line` (string) por épica.
|
|
266
|
+
|
|
262
267
|
3. **Lee los avisos** (`warnings` del bundle y stderr): capa ausente, dependencia inexistente,
|
|
263
268
|
ciclo, línea de release que referencia una épica desconocida. Corrígelos en discovery — el arnés
|
|
264
269
|
**no inventa** el grafo ni edita `epicas.md` (salvo el carve-out de la épica caparazón, Fase 2c).
|
|
265
270
|
|
|
266
|
-
4. **Entrega
|
|
267
|
-
|
|
271
|
+
4. **Entrega al ADMIN un solo fichero.** Si vas a migrar historial, corre `trycore-build migrate`:
|
|
272
|
+
embebe el grafo como sección `graph` del bundle de estado (un único fichero para la pantalla de
|
|
273
|
+
import del proyecto). Sin historial que migrar, entrega `graph-bundle.json` con esta instrucción
|
|
274
|
+
literal: *«súbelo en la consola del hub, pantalla de import del proyecto»*. El import es
|
|
275
|
+
idempotente y **reanuda** si falló a medias.
|
|
276
|
+
|
|
277
|
+
Si el proyecto tiene épicas construidas **antes** de instalar el arnés (sin slice en el
|
|
278
|
+
historial del estado local), decláralas en un fichero que **el humano confirme** y pásalo con
|
|
279
|
+
`--pre-harness <fichero>` (`{"epics": [{"code": "EP-001", "note": "…opcional…"}]}`): se
|
|
280
|
+
importan archivadas **sin declarar gates**, con un acta de migración honesta — sin esto
|
|
281
|
+
quedan «sin empezar» en el hub y `claim_next` las reparte como trabajo nuevo. `migrate`
|
|
282
|
+
avisa de las épicas del grafo sin slice ni declaración pre-arnés, pero **nunca** las
|
|
283
|
+
añade por su cuenta.
|
|
268
284
|
|
|
269
285
|
5. Modo `legacy` o sin credenciales: salta esta fase; el grafo sigue viviendo en los documentos.
|
|
270
286
|
|
|
@@ -70,8 +70,9 @@ reviewer. Es el gate **no negociable**: sin él, no hay release.
|
|
|
70
70
|
- Reporta **cada** gate medido con
|
|
71
71
|
`bash .claude/hooks/build/release-ops.sh verdict <line> <gate> <pass|fail|na> --evidence-file f`
|
|
72
72
|
(`na` cuando no aplica). El **agregado** lo hace el runtime: **parciales no promueven a `passed`**.
|
|
73
|
-
- Con los seis reportados
|
|
74
|
-
|
|
73
|
+
- Con los seis reportados **en verde, el hub auto-cierra** (`closed_by: AUTO_6_OF_6_PASS`; lo
|
|
74
|
+
anuncia la respuesta del último `verdict`). Si la release queda degradada, el cierre es
|
|
75
|
+
decisión humana en la consola (`release-ops.sh close-hint <line>` imprime la política).
|
|
75
76
|
- Algo ✗ → los hallazgos bloqueantes se corrigen **como un slice normal**
|
|
76
77
|
(`/build:slice` / `building-a-slice`) y se **re-corre** el Release Gate.
|
|
77
78
|
|
|
@@ -83,5 +84,5 @@ reviewer. Es el gate **no negociable**: sin él, no hay release.
|
|
|
83
84
|
vez por release (`O(releases)`).
|
|
84
85
|
- **No dupliques el inner loop**: no se hace TDD ni se cierran gates por slice.
|
|
85
86
|
- **`integration` con deps reales es obligatorio**; no se acepta con todo stubbeado.
|
|
86
|
-
- **El agente reporta
|
|
87
|
+
- **El agente reporta; el verde se auto-cierra; el degradado lo decide un humano** (política A-con-B): ninguna llamada de cierre con token de agente — el auto-cierre lo ejecuta el runtime al agregar, no el cliente.
|
|
87
88
|
- Delega en `releasing-a-version`; **no** la reimplementa. Si algo contradice `METODOLOGIA.md`, gana la metodología.
|
package/dist/cli.js
CHANGED
|
@@ -112,9 +112,19 @@ program
|
|
|
112
112
|
.argument('[directory]', 'Directorio del proyecto', '.')
|
|
113
113
|
.option('--out <path>', 'Ruta de salida del bundle (default: .claude/state/migration-bundle.json)')
|
|
114
114
|
.option('--project-ref <ref>', 'Identificador de proyecto en el hub (default: nombre del directorio)')
|
|
115
|
+
.option('--graph <path>', 'Bundle de grafo (graph-bundle.py) a embeber como sección graph (default: .claude/state/graph-bundle.json si existe)')
|
|
116
|
+
.option('--pre-harness <path>', 'Fichero JSON con las épicas construidas ANTES de instalar el arnés, confirmadas por el humano: {"epics": [{"code": "EP-001", "note": "…"}]} — se importan archivadas sin declarar gates, con acta de migración')
|
|
117
|
+
.option('--verify', 'Solo verifica el bundle normalizado contra los invariantes del hub (offline, no escribe nada); exit ≠0 si hay violaciones')
|
|
115
118
|
.action(async (directory, opts) => {
|
|
116
119
|
try {
|
|
117
|
-
await migrate({
|
|
120
|
+
await migrate({
|
|
121
|
+
targetDir: directory,
|
|
122
|
+
outFile: opts.out,
|
|
123
|
+
projectRef: opts.projectRef,
|
|
124
|
+
graphFile: opts.graph,
|
|
125
|
+
preHarnessFile: opts.preHarness,
|
|
126
|
+
verifyOnly: Boolean(opts.verify),
|
|
127
|
+
});
|
|
118
128
|
}
|
|
119
129
|
catch (err) {
|
|
120
130
|
console.error('✗ Error:', err.message);
|
package/dist/commands/migrate.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { targetPaths } from '../lib/paths.js';
|
|
7
|
-
import { buildStateBundle } from '../lib/state-bundle.js';
|
|
7
|
+
import { buildStateBundle, extractGraphSection, graphEpicsWithoutRepresentation, verifyStateBundle, } from '../lib/state-bundle.js';
|
|
8
|
+
import { parsePreHarnessList } from '../lib/normalize.js';
|
|
8
9
|
export async function migrate(opts) {
|
|
9
10
|
const targetDir = path.resolve(opts.targetDir);
|
|
10
11
|
const t = targetPaths(targetDir);
|
|
@@ -20,14 +21,107 @@ export async function migrate(opts) {
|
|
|
20
21
|
console.error(`✗ build-state.json no parsea como JSON: ${err.message}`);
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
24
|
+
// Épicas pre-arnés (issue #42): construidas ANTES de instalar el arnés, sin
|
|
25
|
+
// entrada en history[] — importadas sin esto quedan «sin empezar» en el hub
|
|
26
|
+
// y claim_next las reparte como trabajo nuevo (incidente del primer piloto).
|
|
27
|
+
// La lista la escribe/confirma el humano; nunca se infiere del grafo.
|
|
28
|
+
let preHarness;
|
|
29
|
+
if (opts.preHarnessFile) {
|
|
30
|
+
const preHarnessFile = path.resolve(opts.preHarnessFile);
|
|
31
|
+
if (!fs.existsSync(preHarnessFile)) {
|
|
32
|
+
console.error(`✗ No existe el fichero pre-arnés: ${preHarnessFile}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
let preHarnessRaw;
|
|
36
|
+
try {
|
|
37
|
+
preHarnessRaw = JSON.parse(fs.readFileSync(preHarnessFile, 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error(`✗ El fichero pre-arnés no parsea como JSON: ${err.message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const { list, errors } = parsePreHarnessList(preHarnessRaw);
|
|
44
|
+
if (list === null || errors.length > 0) {
|
|
45
|
+
console.error(`✗ El fichero pre-arnés (${preHarnessFile}) no es válido:`);
|
|
46
|
+
for (const e of errors)
|
|
47
|
+
console.error(` ✗ ${e}`);
|
|
48
|
+
console.error(' Forma esperada: {"epics": [{"code": "EP-001", "note": "…opcional…"}], "as_of": "…ISO opcional…"}');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
preHarness = list;
|
|
52
|
+
}
|
|
23
53
|
const projectRef = opts.projectRef ?? path.basename(targetDir);
|
|
24
|
-
const bundle = buildStateBundle(raw, projectRef);
|
|
54
|
+
const bundle = buildStateBundle(raw, projectRef, preHarness);
|
|
55
|
+
// Sección `graph` embebida (issue #41): el import del hub la consume DENTRO del
|
|
56
|
+
// bundle de estado (ImportBundleIn.graph) — un fichero de grafo aparte se ignora
|
|
57
|
+
// y la historia falla («la épica no existe en el grafo»). El grafo se genera con
|
|
58
|
+
// scripts/lib/graph-bundle.py (flujo de /build:onboard); aquí solo se embebe.
|
|
59
|
+
const defaultGraphFile = path.join(t.stateDir, 'graph-bundle.json');
|
|
60
|
+
const graphFile = opts.graphFile
|
|
61
|
+
? path.resolve(opts.graphFile)
|
|
62
|
+
: fs.existsSync(defaultGraphFile)
|
|
63
|
+
? defaultGraphFile
|
|
64
|
+
: undefined;
|
|
65
|
+
if (graphFile) {
|
|
66
|
+
if (!fs.existsSync(graphFile)) {
|
|
67
|
+
console.error(`✗ No existe el bundle de grafo: ${graphFile}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
let graphRaw;
|
|
71
|
+
try {
|
|
72
|
+
graphRaw = JSON.parse(fs.readFileSync(graphFile, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(`✗ El bundle de grafo no parsea como JSON: ${err.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const { graph, errors } = extractGraphSection(graphRaw);
|
|
79
|
+
if (graph === null) {
|
|
80
|
+
console.error(`✗ El bundle de grafo (${graphFile}) no está en el formato del hub — no se escribe el fichero.`);
|
|
81
|
+
for (const e of errors)
|
|
82
|
+
console.error(` ✗ ${e}`);
|
|
83
|
+
console.error(' Regenéralo con scripts/lib/graph-bundle.py (layer FOUNDATIONAL|BUSINESS, historias con `code`, `release_line` por épica).');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
bundle.graph = graph;
|
|
87
|
+
// Síntoma exacto del incidente del piloto: épicas del grafo sin slice en
|
|
88
|
+
// history[] ni declaración pre-arnés quedan «sin empezar» en el hub y
|
|
89
|
+
// claim_next las reparte como trabajo nuevo. SOLO aviso — la lista
|
|
90
|
+
// pre-arnés la confirma el humano, nada se auto-añade.
|
|
91
|
+
const unrepresented = graphEpicsWithoutRepresentation(graph, bundle.history.map((h) => h.epic_code));
|
|
92
|
+
if (unrepresented.length > 0) {
|
|
93
|
+
bundle.warnings.push(`graph: ${unrepresented.length} épica(s) del grafo sin slice en el historial ni declaración pre-arnés (${unrepresented.join(', ')}) — quedarán «sin empezar» en el hub y claim_next las repartirá como trabajo nuevo; si ya están construidas, decláralas en el fichero de --pre-harness (nunca se auto-añaden)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
25
96
|
if (bundle.validation_errors.length > 0) {
|
|
26
97
|
console.error('✗ El bundle normalizado no pasa la validación local — no se escribe el fichero.');
|
|
27
98
|
for (const e of bundle.validation_errors)
|
|
28
99
|
console.error(` ✗ ${e}`);
|
|
29
100
|
process.exit(1);
|
|
30
101
|
}
|
|
102
|
+
// Verificación offline contra los invariantes del hub (issue #40): la misma
|
|
103
|
+
// entrada que el import rechazaría debe salir aquí, no en la consola del ADMIN.
|
|
104
|
+
const violations = verifyStateBundle(bundle);
|
|
105
|
+
if (opts.verifyOnly) {
|
|
106
|
+
if (violations.length === 0) {
|
|
107
|
+
console.log('✓ Verificación offline: el bundle normalizado no viola ningún invariante conocido del hub.');
|
|
108
|
+
console.log(' (No se escribió ningún fichero: --verify solo verifica.)');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.error(`✗ Verificación offline: ${violations.length} violación(es) de los invariantes del hub:`);
|
|
112
|
+
for (const v of violations)
|
|
113
|
+
console.error(` ✗ ${v.source_key}: ${v.message}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (violations.length > 0) {
|
|
117
|
+
// El destino del bundle es un ADMIN: un bundle que el hub rechazaría no se
|
|
118
|
+
// entrega en silencio — se aborta sin escribir, igual que la validación local.
|
|
119
|
+
console.error(`✗ El bundle viola ${violations.length} invariante(s) del hub — no se escribe el fichero.`);
|
|
120
|
+
for (const v of violations)
|
|
121
|
+
console.error(` ✗ ${v.source_key}: ${v.message}`);
|
|
122
|
+
console.error(' Corrige el build-state.json de origen (o reporta el caso al arnés) y reintenta.');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
31
125
|
bundle.generated_at = new Date().toISOString();
|
|
32
126
|
const outFile = opts.outFile ? path.resolve(opts.outFile) : path.join(t.stateDir, 'migration-bundle.json');
|
|
33
127
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
@@ -35,6 +129,17 @@ export async function migrate(opts) {
|
|
|
35
129
|
console.log(`✓ Bundle de estado preparado: ${outFile}`);
|
|
36
130
|
console.log(` ${bundle.history.length} slice(s) de historial · ${bundle.releases.length} release(s) · ` +
|
|
37
131
|
`${bundle.fronts.length} front(s) · ${bundle.facts.length} hecho(s) de proyecto · ${bundle.unmapped.length} entrada(s) no mapeable(s)`);
|
|
132
|
+
if (preHarness) {
|
|
133
|
+
console.log(` Épicas pre-arnés declaradas: ${preHarness.epics.length} (${preHarness.epics.map((e) => e.code).join(', ')}) — archivadas sin declarar gates, con acta de migración`);
|
|
134
|
+
}
|
|
135
|
+
if (bundle.graph) {
|
|
136
|
+
console.log(` Grafo embebido: ${bundle.graph.epics.length} épica(s) (sección graph del bundle — un solo fichero para el ADMIN)`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(' ⚠ Grafo: NO incluido — el hub reportará «Grafo: no incluida» y rechazará la historia si el');
|
|
140
|
+
console.log(' grafo no fue importado antes. Genera el grafo con scripts/lib/graph-bundle.py y pásalo');
|
|
141
|
+
console.log(' con --graph <fichero> (o déjalo en .claude/state/graph-bundle.json).');
|
|
142
|
+
}
|
|
38
143
|
if (bundle.warnings.length > 0) {
|
|
39
144
|
console.log('');
|
|
40
145
|
console.log(' Avisos (no bloquean; corrígelos en discovery si aplica):');
|