@trycore/spec-build-harness 0.8.4 → 0.10.0
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/GOVERNANCE.md +43 -5
- package/INSTALL.md +28 -6
- package/METODOLOGIA.md +65 -10
- package/README.md +41 -7
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +33 -7
- package/agents/build/dor-dod-gatekeeper.md +17 -6
- package/agents/build/ux-fidelity-reviewer.md +4 -1
- package/agents/build/wiring-adversarial-verifier.md +52 -5
- package/commands/build/architect.md +1 -1
- package/commands/build/claim.md +46 -0
- package/commands/build/escalate.md +36 -0
- package/commands/build/front.md +9 -3
- package/commands/build/onboard.md +63 -14
- package/commands/build/prototype.md +23 -0
- package/commands/build/reflect.md +60 -40
- package/commands/build/release.md +10 -7
- package/commands/build/resume.md +33 -13
- package/commands/build/slice.md +32 -27
- package/commands/build/status.md +35 -0
- package/commands/build/work.md +11 -8
- package/config/build-config.template.json +4 -0
- package/dist/cli.js +22 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/init.js +84 -1
- package/dist/commands/migrate.js +48 -0
- package/dist/commands/status.js +34 -0
- package/dist/lib/normalize.js +276 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/runtime-client.js +196 -0
- package/dist/lib/settings-merge.js +3 -3
- package/dist/lib/state-bundle.js +46 -0
- package/docs/commands.md +32 -9
- package/docs/getting-started.md +2 -1
- package/docs/hooks.md +114 -27
- package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
- package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
- package/docs/runtime/protocolo-cliente-runtime.md +109 -34
- package/hooks/build/build-gate-check.sh +21 -0
- package/hooks/build/context-monitor.sh +82 -15
- package/hooks/build/context-sync.sh +192 -0
- package/hooks/build/design-source-guard.sh +31 -3
- package/hooks/build/dual-compare.sh +92 -0
- package/hooks/build/event-emitter.sh +75 -0
- package/hooks/build/gitflow-guard.sh +164 -14
- package/hooks/build/heartbeat.sh +259 -0
- package/hooks/build/lib/agent-context.sh +139 -0
- package/hooks/build/lib/config.sh +27 -0
- package/hooks/build/lib/projection.sh +71 -0
- package/hooks/build/lib/runtime-client.sh +465 -0
- package/hooks/build/lib/runtime-ops.sh +221 -0
- package/hooks/build/lib/state-io.sh +5 -18
- package/hooks/build/load-build-state.sh +64 -2
- package/hooks/build/reflect-nudge.sh +15 -0
- package/hooks/build/release-gate-nudge.sh +15 -0
- package/hooks/build/release-ops.sh +164 -0
- package/hooks/build/scaffold-guard.sh +29 -2
- package/hooks/build/session-start.sh +103 -0
- package/hooks/build/session-stop.sh +22 -0
- package/hooks/build/slice-ops.sh +877 -0
- package/hooks/build/stack-guard.sh +8 -0
- package/hooks/build/statusline-bridge.sh +24 -3
- package/hooks/build-harness.json +16 -0
- package/package.json +3 -3
- package/scripts/check-agnostic.sh +3 -1
- package/scripts/check-pack-clean.sh +31 -0
- package/scripts/check-runtime-purity.sh +43 -0
- package/scripts/lib/front-plan.py +4 -0
- package/scripts/lib/graph-bundle.py +133 -0
- package/scripts/runtime-purity-allow.txt +5 -0
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/lib/http-stub.py +46 -0
- package/scripts/tests/test-baseline-verdict.sh +92 -0
- package/scripts/tests/test-config.sh +25 -0
- package/scripts/tests/test-hooks-runtime.sh +853 -0
- package/scripts/tests/test-install.sh +57 -0
- package/scripts/tests/test-runtime-client.sh +298 -0
- package/scripts/tests/test-schema.sh +29 -1
- package/scripts/tests/test-skill-ops.sh +847 -0
- package/skills/building-a-micro-change/SKILL.md +22 -4
- package/skills/building-a-slice/SKILL.md +58 -23
- package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
- package/skills/building-a-slice/references/dod.md +12 -3
- package/skills/building-a-slice/references/dor.md +7 -3
- package/skills/building-a-slice/references/evidence-budget.md +51 -0
- package/skills/building-a-slice/references/exploration-fanout.md +1 -1
- package/skills/building-a-slice/references/gitflow.md +1 -1
- package/skills/building-a-slice/references/regression-baseline.md +67 -0
- package/skills/building-a-slice/references/runtime-protocol.md +75 -0
- package/skills/building-a-slice/references/state-protocol.md +12 -1
- package/skills/building-a-slice/workflows/README.md +7 -3
- package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
- package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
- package/skills/managing-parallel-front/SKILL.md +32 -16
- package/skills/openspec-archive-change/SKILL.md +15 -0
- package/skills/prototyping-screens/SKILL.md +104 -0
- package/skills/prototyping-screens/assets/DESIGN.md.template +55 -0
- package/skills/prototyping-screens/assets/manifest.schema.json +70 -0
- package/skills/prototyping-screens/assets/screen.template.html +34 -0
- package/skills/prototyping-screens/references/aesthetic-directions.md +42 -0
- package/skills/prototyping-screens/references/extraction.md +57 -0
- package/skills/prototyping-screens/references/self-check.md +40 -0
- package/skills/releasing-a-version/SKILL.md +25 -16
- package/skills/releasing-a-version/references/release-dod.md +7 -5
- package/skills/releasing-a-version/workflows/README.md +2 -1
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
- package/skills/setup-architecture/SKILL.md +4 -2
- package/state/README.md +20 -3
- package/state/build-state.schema.json +3 -2
- package/templates/CLAUDE.md.template +17 -1
- package/templates/settings-hooks.template.json +8 -4
- package/internal/skills/auditar-arnes/SKILL.md +0 -29
package/dist/lib/paths.js
CHANGED
|
@@ -43,6 +43,7 @@ export const ASSETS = {
|
|
|
43
43
|
buildConfigTemplate: path.join(PACKAGE_ROOT, 'config', 'build-config.template.json'),
|
|
44
44
|
templateClaudeMd: path.join(PACKAGE_ROOT, 'templates', 'CLAUDE.md.template'),
|
|
45
45
|
settingsHooksTemplate: path.join(PACKAGE_ROOT, 'templates', 'settings-hooks.template.json'),
|
|
46
|
+
assetTypes: path.join(PACKAGE_ROOT, 'asset-types.json'),
|
|
46
47
|
};
|
|
47
48
|
/** Rutas dentro del proyecto consumidor (relativas a `targetDir`). */
|
|
48
49
|
export function targetPaths(targetDir) {
|
|
@@ -64,6 +65,11 @@ export function targetPaths(targetDir) {
|
|
|
64
65
|
stateSchema: path.join(claudeDir, 'state', 'build-state.schema.json'),
|
|
65
66
|
stateReadme: path.join(claudeDir, 'state', 'README.md'),
|
|
66
67
|
stateFile: path.join(claudeDir, 'state', 'build-state.json'),
|
|
68
|
+
assetTypesFile: path.join(claudeDir, 'asset-types.json'),
|
|
69
|
+
runtimeCredentialsFile: path.join(claudeDir, 'state', 'runtime.credentials'),
|
|
70
|
+
runtimeLockFile: path.join(claudeDir, 'state', 'context.lock'),
|
|
71
|
+
runtimeProjectionFile: path.join(claudeDir, 'state', 'runtime-projection.json'),
|
|
72
|
+
outboxDir: path.join(claudeDir, 'state', 'outbox'),
|
|
67
73
|
settingsFile: path.join(claudeDir, 'settings.json'),
|
|
68
74
|
versionFile: path.join(claudeDir, '.build-harness-version'),
|
|
69
75
|
claudeMd: path.join(targetDir, 'CLAUDE.md'),
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// runtime-client.ts — cliente TS del Agent Orchestrator Runtime, para el CLI (init/doctor/status).
|
|
2
|
+
// Mismo contrato de red que hooks/build/lib/runtime-client.sh (docs/runtime/protocolo-cliente-runtime.md).
|
|
3
|
+
// Fail-open: ninguna función lanza; ante red caída, timeout o JSON corrupto degrada a un
|
|
4
|
+
// resultado distinguible ({ok:false, status:0}) y nunca corrompe el estado local.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { targetPaths } from './paths.js';
|
|
8
|
+
function runtimePaths(targetDir) {
|
|
9
|
+
const t = targetPaths(targetDir);
|
|
10
|
+
return {
|
|
11
|
+
credentials: t.runtimeCredentialsFile,
|
|
12
|
+
lock: t.runtimeLockFile,
|
|
13
|
+
projection: t.runtimeProjectionFile,
|
|
14
|
+
outboxDir: t.outboxDir,
|
|
15
|
+
buildConfigFile: t.buildConfigFile,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** runtime_mode: TRYCORE_RUNTIME_MODE (env) > build-config.json runtime.mode > "legacy". */
|
|
19
|
+
export function runtimeMode(targetDir) {
|
|
20
|
+
const env = process.env.TRYCORE_RUNTIME_MODE;
|
|
21
|
+
const raw = env ?? readBuildConfigMode(targetDir);
|
|
22
|
+
return raw === 'dual' || raw === 'runtime' ? raw : 'legacy';
|
|
23
|
+
}
|
|
24
|
+
function readBuildConfigMode(targetDir) {
|
|
25
|
+
const p = runtimePaths(targetDir).buildConfigFile;
|
|
26
|
+
try {
|
|
27
|
+
const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
28
|
+
return typeof cfg?.runtime?.mode === 'string' ? cfg.runtime.mode : 'legacy';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 'legacy';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Lee las credenciales; fail-open a {} si el fichero falta o es ilegible. */
|
|
35
|
+
export function readCredentials(targetDir) {
|
|
36
|
+
const p = runtimePaths(targetDir).credentials;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
39
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Upsert atómico de un fragmento sobre runtime.credentials (0600, nunca imprime el token).
|
|
47
|
+
* Nunca lanza: devuelve false si la escritura falló.
|
|
48
|
+
*/
|
|
49
|
+
export function mergeCredentials(targetDir, fragment) {
|
|
50
|
+
const p = runtimePaths(targetDir).credentials;
|
|
51
|
+
const dir = path.dirname(p);
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
const current = readCredentials(targetDir);
|
|
55
|
+
const merged = { ...current, ...fragment };
|
|
56
|
+
const tmp = path.join(dir, `.runtime-credentials.${process.pid}.${Date.now()}.tmp`);
|
|
57
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
58
|
+
fs.chmodSync(tmp, 0o600);
|
|
59
|
+
fs.renameSync(tmp, p);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const TIMEOUT_MS = 10_000;
|
|
67
|
+
/** Primitivo HTTP: nunca lanza. status=0 ante red caída/timeout/URL ausente. */
|
|
68
|
+
async function request(url, method, token, body) {
|
|
69
|
+
if (!url)
|
|
70
|
+
return { ok: false, status: 0, body: null, raw: '' };
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
73
|
+
try {
|
|
74
|
+
const headers = {};
|
|
75
|
+
if (token)
|
|
76
|
+
headers.Authorization = `Bearer ${token}`;
|
|
77
|
+
if (body !== undefined)
|
|
78
|
+
headers['Content-Type'] = 'application/json';
|
|
79
|
+
const res = await fetch(url, {
|
|
80
|
+
method,
|
|
81
|
+
headers,
|
|
82
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
});
|
|
85
|
+
const raw = await res.text();
|
|
86
|
+
let parsed = null;
|
|
87
|
+
try {
|
|
88
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
parsed = null;
|
|
92
|
+
}
|
|
93
|
+
return { ok: res.ok, status: res.status, body: parsed, raw };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { ok: false, status: 0, body: null, raw: '' };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* POST /agents/register — registra el agente CLI (bootstrap, fuera de una sesión Claude).
|
|
104
|
+
* En éxito (200), hace upsert de las credenciales devueltas. 409 -> incompatible=true
|
|
105
|
+
* (no reintentar sin actualizar el paquete).
|
|
106
|
+
*/
|
|
107
|
+
export async function registerAgent(targetDir, opts) {
|
|
108
|
+
const ok0 = mergeCredentials(targetDir, { runtime_url: opts.url, project_token: opts.token });
|
|
109
|
+
if (!ok0)
|
|
110
|
+
return { ok: false, status: 0, incompatible: false, message: 'no se pudieron escribir las credenciales locales' };
|
|
111
|
+
const res = await request(`${opts.url}/agents/register`, 'POST', opts.token, {
|
|
112
|
+
harness_version: opts.harnessVersion,
|
|
113
|
+
asset_types: opts.assetTypes ?? [],
|
|
114
|
+
});
|
|
115
|
+
if (res.status === 200 && res.body && typeof res.body === 'object') {
|
|
116
|
+
const d = res.body;
|
|
117
|
+
const frag = {};
|
|
118
|
+
for (const k of ['agent_key', 'project_id', 'poll_interval_s', 'lease_ttl_s', 'manifest_hash']) {
|
|
119
|
+
if (k in d)
|
|
120
|
+
frag[k] = d[k];
|
|
121
|
+
}
|
|
122
|
+
mergeCredentials(targetDir, frag);
|
|
123
|
+
return { ok: true, status: 200, incompatible: false };
|
|
124
|
+
}
|
|
125
|
+
if (res.status === 409) {
|
|
126
|
+
const minV = res.body?.min_version;
|
|
127
|
+
return { ok: false, status: 409, incompatible: true, message: minV ? `versión mínima requerida: ${minV}` : 'incompatible_version' };
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, status: res.status, incompatible: false, message: res.status === 0 ? 'sin red / runtime inalcanzable' : `rechazo del servidor (${res.status})` };
|
|
130
|
+
}
|
|
131
|
+
/** GET /agent/context — refresco de proyección. Nunca escribe caché (eso es de las skills/hooks). */
|
|
132
|
+
export async function getAgentContext(targetDir) {
|
|
133
|
+
const creds = readCredentials(targetDir);
|
|
134
|
+
if (!creds.runtime_url)
|
|
135
|
+
return { ok: false, status: 0, body: null, raw: '' };
|
|
136
|
+
return request(`${creds.runtime_url}/agent/context`, 'GET', creds.project_token);
|
|
137
|
+
}
|
|
138
|
+
/** Lectura fail-open de la caché de proyección (.claude/state/runtime-projection.json). */
|
|
139
|
+
export function readProjection(targetDir) {
|
|
140
|
+
const p = runtimePaths(targetDir).projection;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
143
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Lectura fail-open del lockfile de contexto (.claude/state/context.lock). */
|
|
150
|
+
export function readLock(targetDir) {
|
|
151
|
+
const p = runtimePaths(targetDir).lock;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
154
|
+
return {
|
|
155
|
+
manifestHash: typeof parsed?.manifest_hash === 'string' ? parsed.manifest_hash : null,
|
|
156
|
+
syncedAt: typeof parsed?.synced_at === 'string' ? parsed.synced_at : null,
|
|
157
|
+
fileCount: Array.isArray(parsed?.files) ? parsed.files.length : 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { manifestHash: null, syncedAt: null, fileCount: 0 };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Estadísticas fail-open de la cola offline (.claude/state/outbox/*.json, sin dotfiles). */
|
|
165
|
+
export function outboxStats(targetDir) {
|
|
166
|
+
const dir = runtimePaths(targetDir).outboxDir;
|
|
167
|
+
try {
|
|
168
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('.'));
|
|
169
|
+
let totalBytes = 0;
|
|
170
|
+
let oldestMs = null;
|
|
171
|
+
for (const f of files) {
|
|
172
|
+
const st = fs.statSync(path.join(dir, f));
|
|
173
|
+
totalBytes += st.size;
|
|
174
|
+
if (oldestMs === null || st.mtimeMs < oldestMs)
|
|
175
|
+
oldestMs = st.mtimeMs;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
count: files.length,
|
|
179
|
+
totalBytes,
|
|
180
|
+
oldestAgeSeconds: oldestMs === null ? null : Math.floor((Date.now() - oldestMs) / 1000),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return { count: 0, totalBytes: 0, oldestAgeSeconds: null };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** ¿El host resuelve una URL http(s) mínimamente bien formada? Validación local, sin red. */
|
|
188
|
+
export function isPlausibleUrl(url) {
|
|
189
|
+
try {
|
|
190
|
+
const u = new URL(url);
|
|
191
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -24,13 +24,13 @@ function cmd(script) {
|
|
|
24
24
|
const STATUSLINE_CMD = cmd('statusline-bridge.sh');
|
|
25
25
|
/** Las agrupaciones de hooks del arnés (= las de hooks/build-harness.json). */
|
|
26
26
|
const HOOK_SPECS = [
|
|
27
|
-
{ event: 'SessionStart', matcher: 'startup|clear|compact', scripts: ['load-build-state.sh'] },
|
|
27
|
+
{ event: 'SessionStart', matcher: 'startup|clear|compact', scripts: ['session-start.sh', 'load-build-state.sh'] },
|
|
28
28
|
{ event: 'PreToolUse', matcher: 'Bash', scripts: ['gitflow-guard.sh'] },
|
|
29
29
|
{ event: 'PreToolUse', matcher: 'Write|Edit|MultiEdit', scripts: ['stack-guard.sh', 'scaffold-guard.sh', 'design-source-guard.sh'] },
|
|
30
30
|
{ event: 'PostToolUse', matcher: 'Write|Edit|MultiEdit', scripts: ['lint-typecheck.sh', 'coherence-flag.sh'] },
|
|
31
|
-
{ event: 'PostToolUse', matcher: 'Bash|Edit|Write|MultiEdit|Task', scripts: ['context-monitor.sh'] },
|
|
31
|
+
{ event: 'PostToolUse', matcher: 'Bash|Edit|Write|MultiEdit|Task', scripts: ['context-monitor.sh', 'event-emitter.sh'] },
|
|
32
32
|
{ event: 'PreCompact', matcher: '.*', scripts: ['context-monitor.sh'] },
|
|
33
|
-
{ event: 'Stop', matcher: '.*', scripts: ['build-gate-check.sh', 'reflect-nudge.sh', 'release-gate-nudge.sh', 'context-monitor.sh'] },
|
|
33
|
+
{ event: 'Stop', matcher: '.*', scripts: ['build-gate-check.sh', 'reflect-nudge.sh', 'release-gate-nudge.sh', 'dual-compare.sh', 'context-monitor.sh', 'session-stop.sh'] },
|
|
34
34
|
];
|
|
35
35
|
/** Permisos MÍNIMOS y enumerados [H11]. Nunca permisos amplios (mcp__*, additionalDirectories…). */
|
|
36
36
|
const MIN_PERMISSIONS = [
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// state-bundle.ts — prepara el BUNDLE NORMALIZADO que un ADMIN importa en la
|
|
2
|
+
// consola del hub. Mismo principio de gobierno que scripts/lib/graph-bundle.py
|
|
3
|
+
// (protocolo-cliente-runtime §6, spec §4.1-D rev. 4): el arnés PREPARA y
|
|
4
|
+
// VALIDA en local — nunca sube nada. `migrate` es la contraparte CLI del
|
|
5
|
+
// import histórico (docs/superpowers/specs/2026-08-20-cliente-migrate-normalizacion-eventos-design.md):
|
|
6
|
+
// convierte el build-state.json legacy del piloto en la forma ImportBundleIn
|
|
7
|
+
// que ya entiende el import del hub — eventos del catálogo v2, no snapshots crudos.
|
|
8
|
+
import { normalizeHistory, normalizeReleases, normalizeFront, normalizeFacts, validateNormalizedBundle, } from './normalize.js';
|
|
9
|
+
export const STATE_BUNDLE_VERSION = 2;
|
|
10
|
+
const EPOCH_ISO = new Date(0).toISOString();
|
|
11
|
+
export function buildStateBundle(raw, projectRef) {
|
|
12
|
+
const warnings = [];
|
|
13
|
+
const st = typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
14
|
+
if (raw !== null && (typeof raw !== 'object' || Array.isArray(raw))) {
|
|
15
|
+
warnings.push('build-state.json no es un objeto JSON: se trata como vacío');
|
|
16
|
+
}
|
|
17
|
+
const historyRaw = Array.isArray(st.history) ? st.history : [];
|
|
18
|
+
if (st.history !== undefined && !Array.isArray(st.history)) {
|
|
19
|
+
warnings.push('history no es un array: se descarta (0 slices migrados)');
|
|
20
|
+
}
|
|
21
|
+
const releasesRaw = Array.isArray(st.releases) ? st.releases : [];
|
|
22
|
+
if (st.releases !== undefined && !Array.isArray(st.releases)) {
|
|
23
|
+
warnings.push('releases no es un array: se descarta (0 releases migradas)');
|
|
24
|
+
}
|
|
25
|
+
const { entries: history, unmapped: historyUnmapped } = normalizeHistory(historyRaw);
|
|
26
|
+
const { entries: releases, unmapped: releasesUnmapped } = normalizeReleases(releasesRaw);
|
|
27
|
+
const front = normalizeFront(st.parallel_front ?? null);
|
|
28
|
+
const fronts = front ? [front] : [];
|
|
29
|
+
const facts = normalizeFacts(st, EPOCH_ISO);
|
|
30
|
+
const unmapped = [...historyUnmapped, ...releasesUnmapped];
|
|
31
|
+
const validationErrors = validateNormalizedBundle({ history, releases, fronts });
|
|
32
|
+
return {
|
|
33
|
+
bundle_version: STATE_BUNDLE_VERSION,
|
|
34
|
+
kind: 'state',
|
|
35
|
+
project_ref: projectRef,
|
|
36
|
+
generated_by: 'trycore-build-harness/state-bundle.ts',
|
|
37
|
+
facts,
|
|
38
|
+
history,
|
|
39
|
+
releases,
|
|
40
|
+
fronts,
|
|
41
|
+
unmapped,
|
|
42
|
+
out_of_scope: [],
|
|
43
|
+
warnings,
|
|
44
|
+
validation_errors: validationErrors,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/docs/commands.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Esta referencia cubre los **dos planos de operación** del arnés de construcción:
|
|
4
4
|
|
|
5
|
-
1. El **CLI `trycore-build`** (binario Node, paquete `@trycore/spec-build-harness`
|
|
6
|
-
2. Los **slash commands de Claude Code** (`/opsx:*` + los
|
|
5
|
+
1. El **CLI `trycore-build`** (binario Node, paquete `@trycore/spec-build-harness`) — instala, actualiza, diagnostica y desinstala el arnés en el proyecto consumidor. Captura el **stack mecánico** y, opcionalmente, registra el proyecto contra el Agent Orchestrator Runtime.
|
|
6
|
+
2. Los **slash commands de Claude Code** (`/opsx:*` + los 12 `/build:*`: `onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front`, `claim`, `status`, `escalate`) — operan el pipeline de dos loops, resuelven la parametrización **semántica** del dominio y capturan el conocimiento aprendido por slice.
|
|
7
7
|
|
|
8
8
|
> **División de responsabilidades del onboarding (dos capas).** Un binario Node **no puede** escribir la auto-memory de Claude. Por eso `trycore-build init` siembra archivos y captura el stack mecánico (lenguaje/deps, package manager, runtime, ruta del PRD), y el slash command `/build:onboard` —ejecutado por Claude— lee el PRD, pregunta por PII / capa de servicios externos-IA / capa determinista / secretos / decisiones de alto impacto, resuelve los `{{placeholders}}` del bloque marcado de `CLAUDE.md` y escribe la auto-memory.
|
|
9
9
|
|
|
@@ -11,19 +11,20 @@ Esta referencia cubre los **dos planos de operación** del arnés de construcci
|
|
|
11
11
|
|
|
12
12
|
## 1. CLI `trycore-build`
|
|
13
13
|
|
|
14
|
-
Comandos: `init` · `update` · `status` · `uninstall` · `doctor`. Todos aceptan un argumento posicional opcional `[directory]` (por defecto `.`, el directorio actual).
|
|
14
|
+
Comandos: `init` · `update` · `status` · `uninstall` · `doctor` · `migrate`. Todos aceptan un argumento posicional opcional `[directory]` (por defecto `.`, el directorio actual).
|
|
15
15
|
|
|
16
16
|
| Comando | Propósito |
|
|
17
17
|
|---|---|
|
|
18
|
-
| `init [directory]` | Instala el arnés en el proyecto (idempotente: re-correrlo es seguro). Siembra agentes, comandos, skills y hooks; captura el stack mecánico; inserta el bloque marcado en `CLAUDE.md` y `.gitignore`; mergea hooks + permisos mínimos en `settings.json`. **Falla (exit 1)** si faltan requisitos externos duros (`openspec`, `python3`, `git`), salvo `--skip-doctor`. |
|
|
18
|
+
| `init [directory]` | Instala el arnés en el proyecto (idempotente: re-correrlo es seguro). Siembra agentes, comandos, skills y hooks; captura el stack mecánico; inserta el bloque marcado en `CLAUDE.md` y `.gitignore`; mergea hooks + permisos mínimos en `settings.json`. Con `--runtime-url`/`--runtime-token` (opcional, beta — EP-OR-08), registra el agente contra el Agent Orchestrator Runtime y dispara el primer sync de contexto; sin esas flags, el proyecto queda en modo `legacy` (default). **Falla (exit 1)** si faltan requisitos externos duros (`openspec`, `python3`, `git`), salvo `--skip-doctor`. |
|
|
19
19
|
| `update [directory]` | Refresca assets y schema tras actualizar el paquete npm. Alias no interactivo de `init` en modo `update`. **Nunca** pisa `build-state.json` ni `stack-allowlist.json` (estado y allowlist son del consumidor). |
|
|
20
|
-
| `status [directory]` | Muestra estado de la instalación (versión instalada vs. paquete, drift), conteo de componentes, requisitos externos y la fase actual del arnés
|
|
20
|
+
| `status [directory]` | Muestra estado de la instalación (versión instalada vs. paquete, drift), conteo de componentes, requisitos externos y la fase actual del arnés. En modo `legacy`, la lee de `build-state.json`; en `dual`/`runtime`, además muestra conectividad con el runtime y la caché de proyección. |
|
|
21
21
|
| `uninstall [directory]` | Quita **solo** lo que `init` puso (agentes, comandos, skills del arnés, hooks, bloques marcados, permisos en `settings.json`). **Preserva** `.claude/state/` y `.claude/config/` (estado vivo y allowlist del equipo). |
|
|
22
|
-
| `doctor [directory]` | Verifica requisitos externos (`openspec`/`python3`/`git`), bit ejecutable de los hooks y la detección de doble canal en `settings.json`. **Falla (exit 1)** si falta un requisito duro. |
|
|
22
|
+
| `doctor [directory]` | Verifica requisitos externos (`openspec`/`python3`/`git`), bit ejecutable de los hooks y la detección de doble canal en `settings.json`. En `dual`/`runtime`, además verifica token, conectividad, frescura del lock de contexto y tamaño de la cola offline. **Falla (exit 1)** si falta un requisito duro. |
|
|
23
|
+
| `migrate [directory]` | (Beta — EP-OR-08.) Prepara el **bundle de estado** (`history[]`/`releases[]`/hechos de proyecto de `build-state.json`) para que un **ADMIN** lo importe en la consola del hub. **Nunca sube nada**: el import es acto de gobierno. `--out <path>` (default `.claude/state/migration-bundle.json`) · `--project-ref <ref>`. |
|
|
23
24
|
|
|
24
25
|
### Flags
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
`init`, `update` y `migrate` aceptan flags. `status`, `uninstall` y `doctor` toman únicamente `[directory]`.
|
|
27
28
|
|
|
28
29
|
| Flag | Comandos | Propósito |
|
|
29
30
|
|---|---|---|
|
|
@@ -35,6 +36,11 @@ Solo `init` y `update` aceptan flags. `status`, `uninstall` y `doctor` toman ún
|
|
|
35
36
|
| `--pkg-manager <pm>` | `init` | Package manager: `npm` \| `pnpm` \| `yarn` (default `npm`). |
|
|
36
37
|
| `--runtime <semver>` | `init` | Semver del runtime, p. ej. `">=18.18"` (default `>=18.18`). |
|
|
37
38
|
| `--prd-path <path>` | `init` | Ruta#ancla del PRD técnico, fuente del allowlist (p. ej. `docs/01-prd/<tu-prd>.md#requisitos-tecnicos`). |
|
|
39
|
+
| `--runtime-url <url>` | `init` | (Beta.) URL del Agent Orchestrator Runtime. Junto con `--runtime-token`, registra el agente y hace el primer sync de contexto. Un runtime inalcanzable **nunca aborta** `init`: degrada a `legacy` y se reintenta en la primera sesión Claude. |
|
|
40
|
+
| `--runtime-token <token>` | `init` | (Beta.) Token de proyecto emitido por un ADMIN en la consola del hub. Se guarda en `.claude/state/runtime.credentials` (0600, gitignored). |
|
|
41
|
+
| `--runtime-mode <mode>` | `init` | (Beta.) `legacy`\|`dual`\|`runtime` tras un registro exitoso (default `dual` si diste URL+token). Ver `docs/runtime/guia-modo-dual-y-migracion.md`. |
|
|
42
|
+
| `--out <path>` | `migrate` | Ruta de salida del bundle de estado (default `.claude/state/migration-bundle.json`). |
|
|
43
|
+
| `--project-ref <ref>` | `migrate` | Identificador del proyecto en el hub (default: nombre del directorio). |
|
|
38
44
|
|
|
39
45
|
> Si pasas todos los datos del stack por flags (o usas `--yes`, o no hay TTY), `init` **no** abre prompt interactivo y usa flags/defaults; deja un aviso para ajustar `.claude/config/stack-allowlist.json` o correr `/build:onboard` después.
|
|
40
46
|
|
|
@@ -52,7 +58,7 @@ Solo `init` y `update` aceptan flags. `status`, `uninstall` y `doctor` toman ún
|
|
|
52
58
|
|
|
53
59
|
## 2. Slash commands de Claude Code
|
|
54
60
|
|
|
55
|
-
El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y `.claude/commands/build/` → los
|
|
61
|
+
El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y `.claude/commands/build/` → los 12 `/build:*` (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front`, `claim`, `status`, `escalate`).
|
|
56
62
|
|
|
57
63
|
### `/opsx:*` — pipeline OpenSpec
|
|
58
64
|
|
|
@@ -117,6 +123,23 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
|
|
|
117
123
|
|---|---|
|
|
118
124
|
| `/build:front` | Abre y coordina un **front paralelo** de épicas NO fundacionales y disjuntas en archivos, cada una en su propio worktree/rama/PR. Delega en la skill `managing-parallel-front`: verifica precondiciones (scaffold confirmado, sin épica foundational abierta), selecciona el conjunto disjunto (`scripts/lib/front-plan.py`) y mergea en orden con re-smoke. Úsalo solo con ≥2 épicas no fundacionales disjuntas listas; para una sola épica, usa `/build:slice`. |
|
|
119
125
|
|
|
126
|
+
### `/build:prototype` — prototipo HTML de referencia (fuente de diseño)
|
|
127
|
+
|
|
128
|
+
| Slash command | Propósito |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `/build:prototype` | Genera o amplía el **prototipo HTML de referencia** (el `DESIGN_SOURCE`) en `docs/05-prototipo/` (`DESIGN.md` + `tokens.css` + `manifest.json` + un HTML autocontenido por pantalla). Adaptador delgado: **delega** en la skill `prototyping-screens`. Dos modos — **greenfield** (`/build:prototype`): inventario de pantallas desde PRD/mapa/historias → confirmación humana → dirección estética (manual de marca o 2-3 variantes a elección humana) → generación por lotes con auto-verificación visual; **feature** (`/build:prototype <épica>`): pantallas nuevas coherentes con el UX/UI **ya implementado**, con **precondición dura** (app corriendo + MCP de inspección de UI: extrae CSS computado real, screenshots en 3 viewports y estructura; sin degradación estática). Es **outer-loop** (antes de abrir slices). **La skill genera; el humano aprueba**: las pantallas nacen `borrador`, solo las `aprobada` son fuente de verdad (las lee `ux-fidelity-reviewer` vía `manifest.json`) y `design_source.confirmed` sigue siendo humano. |
|
|
131
|
+
|
|
132
|
+
### `/build:claim`, `/build:status`, `/build:escalate` — superficie de agente del runtime (beta, opt-in)
|
|
133
|
+
|
|
134
|
+
Adaptadores delgados sobre `slice-ops.sh`; en modo `legacy` (default) devuelven `rc 3` y remiten al
|
|
135
|
+
protocolo del fichero (`/build:slice`) — no rompen el flujo normal si el proyecto no está migrado.
|
|
136
|
+
|
|
137
|
+
| Slash command | Propósito |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `/build:claim [EP-XXX]` | Pide la siguiente tarea al runtime (`POST /tasks/next`): sincroniza contexto, reporta hashes locales (detección de drift) y reclama por lease. Si trae `CHECKPOINT`, continúa desde ahí — **nunca reinicia** un slice de otro agente. |
|
|
140
|
+
| `/build:status` | Informe de solo lectura: modo, slice activo, gates, wiring failing, versión de contexto, lease y cola de eventos pendientes. Nunca transiciona nada. |
|
|
141
|
+
| `/build:escalate <razón>` | Registra un bloqueo en el runtime (`escalation_raised`) y devuelve la decisión a un humano — recortar, diferir o desbloquear **nunca** lo decide el modelo (mismo principio que la regla 8 de METODOLOGIA §10). |
|
|
142
|
+
|
|
120
143
|
---
|
|
121
144
|
|
|
122
145
|
## 3. Caveat de canales (CLI vs. plugin)
|
|
@@ -126,7 +149,7 @@ El arnés se distribuye por **dos canales** que coexisten, pero **namespacean di
|
|
|
126
149
|
| Aspecto | Canal **CLI** (canónico) | Canal **Plugin** nativo |
|
|
127
150
|
|---|---|---|
|
|
128
151
|
| Instalación | `npm i -g @trycore/spec-build-harness` → `trycore-build init` | `/plugin marketplace add <repo-github>` → `/plugin install trycore-spec-build-harness@trycore-build` |
|
|
129
|
-
| Namespace de comandos | Por subcarpeta: `/opsx:*` y los
|
|
152
|
+
| Namespace de comandos | Por subcarpeta: `/opsx:*` y los 9 `/build:*` (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front`) | Por nombre del plugin: `/trycore-spec-build-harness:*` (por diseño de Claude Code) |
|
|
130
153
|
| Referencia a agentes | Por su nombre (p. ej. `build-orchestrator`) | Bajo el nombre del plugin |
|
|
131
154
|
| Cross-references internas | ✔ Escritas para este canal (skills invocan `/opsx:*`, agentes por nombre) | Pueden no resolver según están escritas |
|
|
132
155
|
| Recomendación | **Usar este canal para operar un proyecto** | Conveniencia a nivel usuario |
|
package/docs/getting-started.md
CHANGED
|
@@ -48,7 +48,7 @@ npm i -g @fission-ai/openspec @trycore/spec-build-harness
|
|
|
48
48
|
|
|
49
49
|
## 2 · `trycore-build init` (terminal)
|
|
50
50
|
|
|
51
|
-
Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **
|
|
51
|
+
Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **16 skills** (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens` + 10 `openspec-*`), **10 comandos `/opsx:*`** + **9 comandos `/build:*`** (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front`), **13 hooks**, el estado vacío `.claude/state/build-state.json` (gitignored, nunca se sobreescribe) y el bloque `<!-- BEGIN trycore-build-harness -->` en tu `CLAUDE.md` con `{{placeholders}}` sin resolver.
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
trycore-build init
|
|
@@ -121,5 +121,6 @@ Al archivar una épica, la skill te **pregunta** si correr el Release Gate (defa
|
|
|
121
121
|
- **Agentes** → [`docs/agents.md`](agents.md) · **Hooks** → [`docs/hooks.md`](hooks.md) · **Comandos** → [`docs/commands.md`](commands.md)
|
|
122
122
|
- **Metodología** (manda ante cualquier skill) → [`METODOLOGIA.md`](../METODOLOGIA.md)
|
|
123
123
|
- **Extender con MCP/LSP** (opt-in) → [`docs/customization/`](customization/)
|
|
124
|
+
- **Modo runtime** (cliente de un Agent Orchestrator Runtime, beta/opt-in) → [`docs/runtime/guia-modo-dual-y-migracion.md`](runtime/guia-modo-dual-y-migracion.md)
|
|
124
125
|
|
|
125
126
|
> El core es **100% agnóstico** al proyecto. El ejemplo de referencia completo vive aparte en `docs/examples/reference/` (no forma parte del core).
|