@trycore/spec-build-harness 0.14.2 → 0.16.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 +1 -1
- package/INSTALL.md +1 -1
- package/METODOLOGIA.md +15 -4
- package/README.md +43 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +3 -0
- package/commands/build/front.md +8 -4
- package/commands/build/graph-sync.md +129 -0
- package/dist/commands/init.js +3 -0
- package/docs/commands.md +11 -2
- package/docs/getting-started.md +1 -1
- package/docs/hooks.md +4 -2
- package/docs/runtime/guia-modo-dual-y-migracion.md +79 -3
- package/docs/runtime/protocolo-cliente-runtime.md +66 -7
- package/hooks/build/lib/config.sh +56 -8
- package/hooks/build/lib/harness-meta.sh +72 -0
- package/hooks/build/lib/runtime-client.sh +120 -14
- package/hooks/build/lib/runtime-ops.sh +160 -27
- package/hooks/build/release-ops.sh +38 -12
- package/hooks/build/session-start.sh +5 -61
- package/hooks/build/slice-ops.sh +615 -41
- package/package.json +1 -1
- package/scripts/lib/front-plan.py +55 -6
- package/scripts/lib/graph-bundle.py +118 -3
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/test-config.sh +40 -0
- package/scripts/tests/test-front-plan.sh +63 -0
- package/scripts/tests/test-install.sh +2 -1
- package/scripts/tests/test-runtime-client.sh +49 -3
- package/scripts/tests/test-skill-ops.sh +516 -27
- package/scripts/tests/test-worktree-identity.sh +207 -0
- package/skills/managing-parallel-front/SKILL.md +35 -8
|
@@ -21,6 +21,32 @@ OPS_RC_NO_SLICE=4
|
|
|
21
21
|
OPS_RC_OFFLINE=5
|
|
22
22
|
OPS_RC_REJECTED=6
|
|
23
23
|
OPS_RC_NO_WORK=7
|
|
24
|
+
OPS_RC_NOT_MIRRORABLE=8
|
|
25
|
+
OPS_RC_NO_IDENTITY=9
|
|
26
|
+
|
|
27
|
+
# ── Guard de identidad heredada [#74] ────────────────────────────────────────────────
|
|
28
|
+
# Un worktree sin `.claude/state/runtime.credentials` propio hereda el del clon principal por
|
|
29
|
+
# el fallback de `config_root()`: lee bien, pero al ESCRIBIR suplanta — reclama con el
|
|
30
|
+
# `agent_key` ajeno, reporta al hub los hashes del árbol equivocado y comparte su heartbeat.
|
|
31
|
+
# El hub reparte dos épicas a dos agentes sin objeción; el bloqueo del multi-agente en una
|
|
32
|
+
# misma máquina siempre estuvo aquí, en el cliente. Por eso el guard es fail-FAST y no
|
|
33
|
+
# fail-open: la regla general de este cliente («ante la duda, no bloquees el trabajo local»)
|
|
34
|
+
# protege del runtime caído, no de mandar actos firmados con la identidad de otro.
|
|
35
|
+
#
|
|
36
|
+
# ops_guard_own_identity <subcomando> — rc 0 si este árbol tiene identidad propia;
|
|
37
|
+
# OPS_RC_NO_IDENTITY (9) con mensaje accionable si la heredó.
|
|
38
|
+
ops_guard_own_identity() {
|
|
39
|
+
local sub="${1:-esta operación}" main
|
|
40
|
+
config_is_inherited_root || return 0
|
|
41
|
+
main="$(config_main_clone_root)"
|
|
42
|
+
echo "⛔ este worktree no tiene identidad propia: «${sub}» operaría como el agente del clon" >&2
|
|
43
|
+
echo " principal (${main:-?}) — su agent_key, su lease, sus hashes de contexto." >&2
|
|
44
|
+
echo " Da de alta este árbol con un token de agente NUEVO emitido por el ADMIN:" >&2
|
|
45
|
+
echo " bash .claude/hooks/build/slice-ops.sh register" >&2
|
|
46
|
+
echo " (el token se pide por stdin, nunca por argumento). Las lecturas — \`status\`," >&2
|
|
47
|
+
echo " \`next-step\`, \`mode\` — siguen funcionando con el contexto heredado." >&2
|
|
48
|
+
return $OPS_RC_NO_IDENTITY
|
|
49
|
+
}
|
|
24
50
|
|
|
25
51
|
# Tope de texto libre que el cliente manda al servidor (evidencias, notas): el protocolo §8
|
|
26
52
|
# dice que el servidor rechaza payloads sobredimensionados; se trunca aquí, marcándolo.
|
|
@@ -40,6 +66,16 @@ OPS_RELEASE_GATES="security smell ux coherence stack_arch integration"
|
|
|
40
66
|
# (orden estricto, domain.py:400-407) — de aquí salen las cadenas que emite `phase`.
|
|
41
67
|
OPS_SLICE_PHASES="dor change red green refactor smoke api data dod pr"
|
|
42
68
|
|
|
69
|
+
# Enumeración NORMATIVA de la ingesta espejo (`TIPOS_DE_ESPEJO` del hub, derivada allí de los
|
|
70
|
+
# schemas que heredan de `_TransicionDeEspejo`). Se replica aquí para no gastar una llamada en
|
|
71
|
+
# algo que el hub va a rechazar por elemento — y el test la compara contra esta misma lista, así
|
|
72
|
+
# que ampliarla en el hub obliga a ampliarla aquí (lección de #44: dos listas del mismo hecho
|
|
73
|
+
# divergen tarde o temprano, y la que calla es la que miente).
|
|
74
|
+
# `release_verdict_reported` y `front_integration_reported` NO están, y no es un olvido: no
|
|
75
|
+
# existen en el catálogo del hub. Sus canales de espejo son trabajo hub-side (tipo nuevo con su
|
|
76
|
+
# base `_TransicionDeEspejo` y su rama de reducer), no algo que este cliente pueda arreglar.
|
|
77
|
+
OPS_MIRROR_TYPES="slice_opened phase_advanced phase_reverted gate_verdict wiring_item_updated wiring_checklist_seeded checkpoint_recorded progress_noted handoff_recorded"
|
|
78
|
+
|
|
43
79
|
# ops_phase_idx <fase> — índice de la fase en OPS_SLICE_PHASES; -1 si no es fase del hub.
|
|
44
80
|
ops_phase_idx() {
|
|
45
81
|
local i=0 p
|
|
@@ -118,6 +154,14 @@ ops_path_proposals() { echo "/projects/$1/context/agent-proposals"; }
|
|
|
118
154
|
ops_path_epic_proposals() { echo "/projects/$1/agent/epic-proposals"; }
|
|
119
155
|
ops_path_epic_proposal() { echo "/projects/$1/agent/epic-proposals/$2"; }
|
|
120
156
|
|
|
157
|
+
# [EP-OR-17] Re-sync del grafo (HU-OR-72/73). El arnés PROPONE el bundle construido desde los
|
|
158
|
+
# docs de discovery, el hub calcula el delta y un HUMANO aprueba en la consola: nada se aplica
|
|
159
|
+
# por esta ruta. Mismo carril `/agent/` que las propuestas de épica y por la misma razón — la
|
|
160
|
+
# ruta sin `/agent/` es la superficie humana (sesión + PDP) y a un bearer de agente le responde
|
|
161
|
+
# 403 auditado, no 405: las dos superficies no se comparten por diseño.
|
|
162
|
+
ops_path_graph_sync_proposals() { echo "/projects/$1/agent/graph-sync-proposals"; }
|
|
163
|
+
ops_path_graph_sync_proposal() { echo "/projects/$1/agent/graph-sync-proposals/$2"; }
|
|
164
|
+
|
|
121
165
|
ops_mode() { runtime_mode; }
|
|
122
166
|
|
|
123
167
|
# ops_slice_id — id del slice activo según la caché de proyección ("" si no hay).
|
|
@@ -265,41 +309,98 @@ PY
|
|
|
265
309
|
runtime_outbox_enforce_cap >/dev/null
|
|
266
310
|
}
|
|
267
311
|
|
|
268
|
-
# ops_mirror <
|
|
269
|
-
# fichero local sigue siendo primario: un rechazo del reducer NO bloquea el
|
|
270
|
-
# como discrepancia del comparador.
|
|
312
|
+
# ops_mirror <event_type> <payload_file> [epic_code] — ingesta espejo del modo `dual`
|
|
313
|
+
# (spec §4.1-D). El fichero local sigue siendo primario: un rechazo del reducer NO bloquea el
|
|
314
|
+
# trabajo, queda como discrepancia del comparador.
|
|
315
|
+
# rc 0 aplicado o duplicado · 5 no se pudo (red, sin epic_code, sin python3) · 6 rechazado
|
|
316
|
+
# · 8 el tipo no es espejable.
|
|
317
|
+
#
|
|
318
|
+
# [EP-OR-17] El cuerpo es `MirrorTransitionsIn`: `{"transitions":[{client_event_id, epic_code,
|
|
319
|
+
# event_type, payload}]}`. El `{kind, payload, origin, occurred_at}` que se mandaba antes no
|
|
320
|
+
# existe en el contrato — el 100 % de lo que el modo dual espejaba era un 422. El `occurred_at`
|
|
321
|
+
# tampoco viaja, y no por descuido: los relojes de cliente entran SOLO por el import; aquí
|
|
322
|
+
# estampa el servidor. El `origin` tampoco: la marca de origen-espejo la pone el hub, y lo que
|
|
323
|
+
# el cliente dijera no elegiría el origen de todos modos.
|
|
271
324
|
ops_mirror() {
|
|
272
|
-
local
|
|
325
|
+
local event_type="$1" pfile="$2" epic_code="${3:-}" pid body resp status tmp
|
|
326
|
+
case " $OPS_MIRROR_TYPES " in
|
|
327
|
+
*" $event_type "*) : ;;
|
|
328
|
+
*)
|
|
329
|
+
echo "⚠ «${event_type}» está fuera de la enumeración normativa del catálogo de espejo del hub:" >&2
|
|
330
|
+
echo " no se envía (viajaría solo para volver rechazado por elemento). Espejables:" >&2
|
|
331
|
+
echo " $OPS_MIRROR_TYPES" >&2
|
|
332
|
+
return $OPS_RC_NOT_MIRRORABLE ;;
|
|
333
|
+
esac
|
|
273
334
|
pid="$(runtime_field project_id)"
|
|
274
|
-
[ -n "$pid" ] || return
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
335
|
+
[ -n "$pid" ] || return $OPS_RC_OFFLINE
|
|
336
|
+
[ -n "$epic_code" ] || epic_code="$(ops_local_slice_ref | python3 -c 'import json,sys; print(json.load(sys.stdin).get("epic_code") or "")' 2>/dev/null)"
|
|
337
|
+
if [ -z "$epic_code" ]; then
|
|
338
|
+
echo "⚠ «${event_type}» no espejado: sin epic_code (el hub lo exige y el lote sería un 422)." >&2
|
|
339
|
+
echo " Sale de active_slice.epica del build-state local: abre un slice antes de espejar." >&2
|
|
340
|
+
return $OPS_RC_OFFLINE
|
|
341
|
+
fi
|
|
342
|
+
command -v python3 >/dev/null 2>&1 || return $OPS_RC_OFFLINE
|
|
343
|
+
tmp="$(mktemp)" || return $OPS_RC_OFFLINE
|
|
344
|
+
python3 - "$event_type" "$pfile" "$epic_code" > "$tmp" <<'PY' 2>/dev/null
|
|
345
|
+
import json,sys,uuid
|
|
346
|
+
event_type,pfile,epic_code=sys.argv[1],sys.argv[2],sys.argv[3]
|
|
280
347
|
try:
|
|
281
348
|
payload=json.load(open(pfile))
|
|
282
349
|
except Exception:
|
|
283
350
|
payload={}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
351
|
+
if not isinstance(payload,dict):
|
|
352
|
+
payload={}
|
|
353
|
+
print(json.dumps({"transitions":[{"client_event_id":str(uuid.uuid4()),
|
|
354
|
+
"epic_code":epic_code,
|
|
355
|
+
"event_type":event_type,
|
|
356
|
+
"payload":payload}]},ensure_ascii=False))
|
|
287
357
|
PY
|
|
288
358
|
body="$(cat "$tmp" 2>/dev/null)"
|
|
289
359
|
rm -f "$tmp"
|
|
290
|
-
[ -n "$body" ] || return
|
|
360
|
+
[ -n "$body" ] || return $OPS_RC_OFFLINE
|
|
291
361
|
resp="$(runtime_post "$(ops_path_mirror "$pid")" "$body")"
|
|
292
362
|
status="$(runtime_http_status)"
|
|
293
363
|
case "$status" in
|
|
294
|
-
200|201|202)
|
|
364
|
+
200|201|202) _ops_mirror_report "$event_type" "$resp" ;;
|
|
295
365
|
409|422)
|
|
296
|
-
|
|
366
|
+
# 409 = el cutover apagó la ingesta espejo para este proyecto; 422 = el lote no cumple el
|
|
367
|
+
# borde. Los dos son del LOTE entero, no de un elemento: no hay reporte que leer.
|
|
368
|
+
echo "⚠ el espejo rechazó «${event_type}» (HTTP $status): $resp" >&2
|
|
297
369
|
echo " (queda como discrepancia del comparador dual; el trabajo local NO se bloquea)" >&2
|
|
298
|
-
return
|
|
299
|
-
*) return
|
|
370
|
+
return $OPS_RC_REJECTED ;;
|
|
371
|
+
*) return $OPS_RC_OFFLINE ;;
|
|
300
372
|
esac
|
|
301
373
|
}
|
|
302
374
|
|
|
375
|
+
# _ops_mirror_report <event_type> <respuesta> — lee el reporte POR ELEMENTO (HU-OR-56 E3).
|
|
376
|
+
# rc 0 aplicado o duplicado · 6 rechazado, con el motivo del hub a la vista: volcar el cuerpo
|
|
377
|
+
# crudo escondía justo la frase que dice qué hay que arreglar.
|
|
378
|
+
_ops_mirror_report() {
|
|
379
|
+
local event_type="$1" reason
|
|
380
|
+
command -v python3 >/dev/null 2>&1 || return $OPS_RC_OK
|
|
381
|
+
reason="$(printf '%s' "$2" | python3 -c '
|
|
382
|
+
import json,sys
|
|
383
|
+
try:
|
|
384
|
+
d=json.load(sys.stdin)
|
|
385
|
+
rs=d.get("results") or []
|
|
386
|
+
sys.stdout.write(next((r.get("reason") or "rechazado sin motivo registrado")
|
|
387
|
+
for r in rs if r.get("status")=="rejected"))
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
' 2>/dev/null)"
|
|
391
|
+
[ -n "$reason" ] || return $OPS_RC_OK
|
|
392
|
+
echo "⚠ el espejo rechazó «${event_type}»: $reason" >&2
|
|
393
|
+
case "$reason" in
|
|
394
|
+
*"no existe en el grafo"*)
|
|
395
|
+
# Este es EL rechazo del piloto: el grafo del hub se quedó en la foto del import inicial
|
|
396
|
+
# y toda transición de una épica posterior rebota. Ahora hay una salida que ofrecer.
|
|
397
|
+
echo " El grafo del hub no conoce esa épica. Propón el re-sync con" >&2
|
|
398
|
+
echo " \`slice-ops.sh graph-sync --file epics.json\` y pide su aprobación en la consola." >&2 ;;
|
|
399
|
+
esac
|
|
400
|
+
echo " (queda como discrepancia del comparador dual; el trabajo local NO se bloquea)" >&2
|
|
401
|
+
return $OPS_RC_REJECTED
|
|
402
|
+
}
|
|
403
|
+
|
|
303
404
|
# ── Ledger local de propuestas de épica [#62] ────────────────────────────────────────
|
|
304
405
|
# Memoria del ciclo «propongo → un humano aprueba → escribo el fichero con el código del
|
|
305
406
|
# hub». Vive fuera de build-state.json a propósito: en modo `runtime` ese fichero no es la
|
|
@@ -308,11 +409,13 @@ PY
|
|
|
308
409
|
# instante); `proposal_id` llega después, con el ack, y puede no llegar nunca (offline).
|
|
309
410
|
ops_epic_ledger_path() { echo "$(config_root)/.claude/state/epic-proposals.json"; }
|
|
310
411
|
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
412
|
+
# _ops_ledger_read <fichero> — imprime {"proposals":[…]}; nunca lanza.
|
|
413
|
+
# [EP-OR-17] Base común del ledger de propuestas de épica y del de re-sync del grafo: son la
|
|
414
|
+
# misma estructura con otra ruta, y duplicar 60 líneas de python3 garantizaba que una de las
|
|
415
|
+
# dos copias se quedara atrás.
|
|
416
|
+
_ops_ledger_read() {
|
|
314
417
|
command -v python3 >/dev/null 2>&1 || { echo '{"proposals":[]}'; return; }
|
|
315
|
-
python3 - "$
|
|
418
|
+
python3 - "$1" <<'PY' 2>/dev/null || echo '{"proposals":[]}'
|
|
316
419
|
import json,sys
|
|
317
420
|
try:
|
|
318
421
|
d=json.load(open(sys.argv[1]))
|
|
@@ -323,12 +426,11 @@ except Exception:
|
|
|
323
426
|
PY
|
|
324
427
|
}
|
|
325
428
|
|
|
326
|
-
#
|
|
429
|
+
# _ops_ledger_upsert <fichero> <entrada-json> — upsert por client_event_id: las claves que la
|
|
327
430
|
# entrada NO menciona se conservan (así un `epic-status` puede fijar el estado sin borrar el
|
|
328
431
|
# borrador que guardó `propose-epic`). Escritura atómica. rc 0/1.
|
|
329
|
-
|
|
330
|
-
local
|
|
331
|
-
f="$(ops_epic_ledger_path)"
|
|
432
|
+
_ops_ledger_upsert() {
|
|
433
|
+
local f="$1" entry="$2"
|
|
332
434
|
mkdir -p "$(dirname "$f")"
|
|
333
435
|
command -v python3 >/dev/null 2>&1 || return 1
|
|
334
436
|
python3 - "$f" "$entry" <<'PY' 2>/dev/null || return 1
|
|
@@ -351,7 +453,7 @@ for i,p in enumerate(ps):
|
|
|
351
453
|
else:
|
|
352
454
|
ps.append(e)
|
|
353
455
|
dirn=os.path.dirname(path) or "."
|
|
354
|
-
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".
|
|
456
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".ledger.",suffix=".tmp")
|
|
355
457
|
try:
|
|
356
458
|
with os.fdopen(fd,"w") as out:
|
|
357
459
|
json.dump({"proposals":ps},out,indent=2,ensure_ascii=False); out.flush(); os.fsync(out.fileno())
|
|
@@ -363,6 +465,37 @@ except Exception:
|
|
|
363
465
|
PY
|
|
364
466
|
}
|
|
365
467
|
|
|
468
|
+
ops_epic_ledger_read() { _ops_ledger_read "$(ops_epic_ledger_path)"; }
|
|
469
|
+
ops_epic_ledger_upsert() { _ops_ledger_upsert "$(ops_epic_ledger_path)" "$1"; }
|
|
470
|
+
|
|
471
|
+
# ── Ledger local del re-sync del grafo [EP-OR-17] ────────────────────────────────────
|
|
472
|
+
# Fichero PROPIO, no una sección del de épicas: un re-sync no es una propuesta de épica, y
|
|
473
|
+
# mezclarlos haría que `epic-status` hablara de grafos y `graph-sync-status` de épicas.
|
|
474
|
+
ops_graph_sync_ledger_path() { echo "$(config_root)/.claude/state/graph-sync-proposals.json"; }
|
|
475
|
+
ops_graph_sync_ledger_read() { _ops_ledger_read "$(ops_graph_sync_ledger_path)"; }
|
|
476
|
+
ops_graph_sync_ledger_upsert() { _ops_ledger_upsert "$(ops_graph_sync_ledger_path)" "$1"; }
|
|
477
|
+
|
|
478
|
+
# ops_graph_sync_cid — `client_event_id` DETERMINISTA de la próxima propuesta de re-sync:
|
|
479
|
+
# `<agent_key>:graph-sync:<n>`, con n = entradas del ledger + 1.
|
|
480
|
+
#
|
|
481
|
+
# Determinista es el punto: el reintento tras un 409 de grafo rancio reusa exactamente este
|
|
482
|
+
# cid y el hub deduplica, en vez de acabar con dos propuestas del mismo grafo esperando
|
|
483
|
+
# aprobación humana. El contrato acota el campo a 64 caracteres, así que una `agent_key` larga
|
|
484
|
+
# se sustituye por su sha256 truncado — sigue siendo estable para el mismo agente, que es lo
|
|
485
|
+
# único que la idempotencia necesita.
|
|
486
|
+
ops_graph_sync_cid() {
|
|
487
|
+
local ak n
|
|
488
|
+
ak="$(runtime_field agent_key)"
|
|
489
|
+
[ -n "$ak" ] || ak="agente"
|
|
490
|
+
n="$(ops_graph_sync_ledger_read | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("proposals") or []) + 1)' 2>/dev/null)"
|
|
491
|
+
case "$n" in ''|*[!0-9]*) n=1 ;; esac
|
|
492
|
+
if [ "${#ak}" -gt 30 ]; then
|
|
493
|
+
ak="$(printf '%s' "$ak" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.read().encode()).hexdigest()[:16])' 2>/dev/null)"
|
|
494
|
+
[ -n "$ak" ] || ak="agente"
|
|
495
|
+
fi
|
|
496
|
+
echo "${ak}:graph-sync:${n}"
|
|
497
|
+
}
|
|
498
|
+
|
|
366
499
|
# ops_epic_ack_read <client_event_id> — el ack que dejó el carril directo, o "".
|
|
367
500
|
ops_epic_ack_read() {
|
|
368
501
|
local f; f="$(runtime_outbox_dir)/acks/$1.json"
|
|
@@ -24,6 +24,7 @@ uso: release-ops.sh <subcomando> [opciones]
|
|
|
24
24
|
[--resmoke pass|fail] [--evidence T | --evidence-file F]
|
|
25
25
|
|
|
26
26
|
códigos de salida: 0 ok · 2 uso · 3 legacy · 5 offline · 6 rechazado
|
|
27
|
+
· 9 identidad heredada (da de alta el worktree: `slice-ops.sh register`)
|
|
27
28
|
USAGE
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -35,7 +36,7 @@ _rel_evidence_json() {
|
|
|
35
36
|
|
|
36
37
|
# ── verdict ──────────────────────────────────────────────────────────────────────────
|
|
37
38
|
cmd_verdict() {
|
|
38
|
-
local line="${1:-}" gate="${2:-}" estado="${3:-}" ev="" evfile="" mode payload resp status
|
|
39
|
+
local line="${1:-}" gate="${2:-}" estado="${3:-}" ev="" evfile="" mode payload resp status rel_status closed_by
|
|
39
40
|
if [ -z "$line" ] || [ -z "$gate" ] || [ -z "$estado" ]; then usage; return $OPS_RC_USAGE; fi
|
|
40
41
|
shift 3
|
|
41
42
|
while [ $# -gt 0 ]; do
|
|
@@ -59,9 +60,13 @@ cmd_verdict() {
|
|
|
59
60
|
[ "$mode" = "legacy" ] && { echo "LEGACY: escribe el gate en releases[] del fichero"; return $OPS_RC_LEGACY; }
|
|
60
61
|
payload="{\"release_line\":$(ops_json_str "$line"),\"gate\":$(ops_json_str "$gate"),\"status\":$(ops_json_str "$estado"),\"verdict\":$(ops_json_str "$(printf %s "$estado" | tr a-z A-Z)"),\"evidence\":$(_rel_evidence_json "$ev" "$evfile")}"
|
|
61
62
|
if [ "$mode" = "dual" ]; then
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
# [EP-OR-17] El hub NO tiene canal de espejo para los veredictos de release:
|
|
64
|
+
# `release_verdict_reported` no existe en su catálogo de eventos (`TIPOS_DE_ESPEJO` solo
|
|
65
|
+
# cubre transiciones de slice) y su superficie síncrona es de gobierno humano. Mandarlo era
|
|
66
|
+
# gastar una llamada para recibir un rechazo por elemento y un aviso genérico que mandaba a
|
|
67
|
+
# revisar una red que funcionaba. Se dice lo que pasa y se sigue: el fichero es primario.
|
|
68
|
+
echo "DUAL: veredicto anotado en local. El hub todavía no espeja veredictos de release"
|
|
69
|
+
echo "(no hay tipo en su catálogo): quedará reflejado al pasar a modo \`runtime\`."
|
|
65
70
|
return $OPS_RC_OK
|
|
66
71
|
fi
|
|
67
72
|
resp="$(runtime_post "$(ops_path_release_verdicts "$(ops_urlenc "$line")")" "$payload")"
|
|
@@ -131,11 +136,18 @@ HINT
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
# ── front-integration ────────────────────────────────────────────────────────────────
|
|
134
|
-
# Cada worktree se registra como AGENTE PROPIO
|
|
135
|
-
#
|
|
136
|
-
#
|
|
139
|
+
# Cada worktree se registra como AGENTE PROPIO y reporta la integración de SU miembro.
|
|
140
|
+
# Planificar, abrir, drenar y cerrar el front siguen siendo humanos.
|
|
141
|
+
#
|
|
142
|
+
# [#74] «Mismo token de proyecto, agent_key distinto» era falso y estuvo escrito aquí y en la
|
|
143
|
+
# skill: el hub NO crea identidad en `POST /agents/register` — el `agent_key` es el `agent_id`
|
|
144
|
+
# que el ADMIN grabó en el token al emitirlo. Un token por agente, o los dos worktrees son el
|
|
145
|
+
# mismo agente. La compuerta va en el despacho (`ops_guard_own_identity`), no aquí: el
|
|
146
|
+
# `[ -n "$akey" ]` de más abajo era inefectivo por construcción — leía la clave vía
|
|
147
|
+
# `config_root()`, que en un worktree sin credenciales ya había heredado la del principal, así
|
|
148
|
+
# que solo saltaba si el principal TAMPOCO estaba registrado.
|
|
137
149
|
cmd_front_integration() {
|
|
138
|
-
local front="${1:-}" merge="" resmoke="" ev="" evfile="" mode akey payload resp status
|
|
150
|
+
local front="${1:-}" merge="" resmoke="" ev="" evfile="" mode akey payload resp status
|
|
139
151
|
[ -n "$front" ] || { usage; return $OPS_RC_USAGE; }
|
|
140
152
|
shift
|
|
141
153
|
while [ $# -gt 0 ]; do
|
|
@@ -159,13 +171,17 @@ cmd_front_integration() {
|
|
|
159
171
|
[ "$mode" = "legacy" ] && { echo "LEGACY: actualiza parallel_front.members[] en el fichero"; return $OPS_RC_LEGACY; }
|
|
160
172
|
payload="{\"merge_status\":$(ops_json_str "$merge"),\"re_smoke\":{\"status\":$(ops_json_str "$resmoke"),\"evidence\":$(_rel_evidence_json "$ev" "$evfile")}}"
|
|
161
173
|
if [ "$mode" = "dual" ]; then
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
# Mismo caso que los veredictos de release: `front_integration_reported` no existe en el
|
|
175
|
+
# catálogo del hub y el agregado `front` no entra por la ingesta espejo.
|
|
176
|
+
echo "DUAL: integración anotada en local. El hub todavía no espeja integraciones de front"
|
|
177
|
+
echo "(no hay tipo en su catálogo): quedará reflejada al pasar a modo \`runtime\`."
|
|
165
178
|
return $OPS_RC_OK
|
|
166
179
|
fi
|
|
167
180
|
akey="$(runtime_field agent_key)"
|
|
168
|
-
[ -n "$akey" ] || {
|
|
181
|
+
[ -n "$akey" ] || {
|
|
182
|
+
echo "⛔ este árbol no tiene \`agent_key\`: da de alta el agente con" >&2
|
|
183
|
+
echo " \`bash .claude/hooks/build/slice-ops.sh register\` (token propio del ADMIN)." >&2
|
|
184
|
+
return $OPS_RC_REJECTED; }
|
|
169
185
|
resp="$(runtime_post "$(ops_path_front_integration "$(ops_urlenc "$front")" "$(ops_urlenc "$akey")")" "$payload")"
|
|
170
186
|
status="$(runtime_http_status)"
|
|
171
187
|
case "$status" in
|
|
@@ -187,6 +203,16 @@ cmd_front_integration() {
|
|
|
187
203
|
|
|
188
204
|
SUB="${1:-}"
|
|
189
205
|
[ $# -gt 0 ] && shift
|
|
206
|
+
|
|
207
|
+
# [#74] Reportar la integración de un miembro del front es EL acto por-agente de este fichero
|
|
208
|
+
# (la ruta lleva el `agent_key` en el path): desde un árbol con identidad heredada estaría
|
|
209
|
+
# firmando por el agente del clon principal. `verdict` queda fuera de la compuerta a propósito
|
|
210
|
+
# — un veredicto de release es del proyecto, no del agente, y se reporta desde el árbol
|
|
211
|
+
# principal; `close-hint` no toca la red.
|
|
212
|
+
case "$SUB" in
|
|
213
|
+
front-integration) ops_guard_own_identity "front-integration" || exit $? ;;
|
|
214
|
+
esac
|
|
215
|
+
|
|
190
216
|
case "$SUB" in
|
|
191
217
|
verdict) cmd_verdict "$@"; exit $? ;;
|
|
192
218
|
close-hint) cmd_close_hint "$@"; exit $? ;;
|
|
@@ -11,6 +11,9 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
11
11
|
source "$HERE/lib/runtime-client.sh"
|
|
12
12
|
source "$HERE/lib/projection.sh"
|
|
13
13
|
source "$HERE/lib/agent-context.sh"
|
|
14
|
+
# [#74] La versión del arnés y el catálogo de asset types viven en lib/harness-meta.sh:
|
|
15
|
+
# `slice-ops.sh register` los necesita exactamente iguales para dar de alta un worktree.
|
|
16
|
+
source "$HERE/lib/harness-meta.sh"
|
|
14
17
|
|
|
15
18
|
cat >/dev/null 2>&1 # consumir el payload del hook (protocolo); no se usa aquí
|
|
16
19
|
|
|
@@ -19,67 +22,8 @@ MODE="$(runtime_mode)"
|
|
|
19
22
|
command -v python3 >/dev/null 2>&1 || exit 0
|
|
20
23
|
[ -n "$(runtime_url)" ] || exit 0 # sin `trycore-build init` (sub-slice D) no hay runtime
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# `readlink -f` no es portable al BSD readlink de macOS.
|
|
25
|
-
sst_pkg_root() {
|
|
26
|
-
local src="${BASH_SOURCE[0]}" target i=0
|
|
27
|
-
while [ -L "$src" ] && [ "$i" -lt 10 ]; do
|
|
28
|
-
target="$(readlink "$src" 2>/dev/null)" || break
|
|
29
|
-
case "$target" in
|
|
30
|
-
/*) src="$target" ;;
|
|
31
|
-
*) src="$(dirname "$src")/$target" ;;
|
|
32
|
-
esac
|
|
33
|
-
i=$((i + 1))
|
|
34
|
-
done
|
|
35
|
-
(cd "$(dirname "$src")/../.." >/dev/null 2>&1 && pwd -P) 2>/dev/null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# sst_harness_version — versión instalada en el consumidor; si no, la del paquete.
|
|
39
|
-
sst_harness_version() {
|
|
40
|
-
local f pkg
|
|
41
|
-
f="$(config_root)/.claude/.build-harness-version"
|
|
42
|
-
if [ -f "$f" ]; then
|
|
43
|
-
tr -d ' \n' < "$f"
|
|
44
|
-
return
|
|
45
|
-
fi
|
|
46
|
-
pkg="$(sst_pkg_root)"
|
|
47
|
-
if [ -n "$pkg" ] && [ -f "$pkg/VERSION" ]; then
|
|
48
|
-
tr -d ' \n' < "$pkg/VERSION"
|
|
49
|
-
return
|
|
50
|
-
fi
|
|
51
|
-
echo "0.0.0"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# sst_asset_types — el array `types` de asset-types.json del paquete (protocolo §6).
|
|
55
|
-
# Cadena de resolución: plugin > copia en .claude/ (la siembra es del sub-slice D) >
|
|
56
|
-
# paquete resuelto por symlink. Sin fichero: "[]" — el registro sigue funcionando.
|
|
57
|
-
sst_asset_types() {
|
|
58
|
-
local pkg candidates f
|
|
59
|
-
pkg="$(sst_pkg_root)"
|
|
60
|
-
candidates="${CLAUDE_PLUGIN_ROOT:-}/asset-types.json
|
|
61
|
-
$(config_root)/.claude/asset-types.json
|
|
62
|
-
${pkg:-}/asset-types.json"
|
|
63
|
-
while IFS= read -r f; do
|
|
64
|
-
case "$f" in /asset-types.json|asset-types.json) continue ;; esac
|
|
65
|
-
[ -f "$f" ] || continue
|
|
66
|
-
python3 - "$f" <<'PY' 2>/dev/null && return
|
|
67
|
-
import json,sys
|
|
68
|
-
try:
|
|
69
|
-
d=json.load(open(sys.argv[1]))
|
|
70
|
-
t=d.get("types")
|
|
71
|
-
print(json.dumps(t if isinstance(t,list) else [],ensure_ascii=False))
|
|
72
|
-
except Exception:
|
|
73
|
-
raise SystemExit(1)
|
|
74
|
-
PY
|
|
75
|
-
done <<EOS
|
|
76
|
-
$candidates
|
|
77
|
-
EOS
|
|
78
|
-
echo "[]"
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
HV="$(sst_harness_version)"
|
|
82
|
-
AT="$(sst_asset_types)"
|
|
25
|
+
HV="$(harness_version)"
|
|
26
|
+
AT="$(harness_asset_types)"
|
|
83
27
|
|
|
84
28
|
runtime_register "$HV" "$AT"
|
|
85
29
|
RC=$?
|