@trycore/spec-build-harness 0.15.0 → 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 +42 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +3 -0
- package/commands/build/front.md +8 -4
- package/docs/commands.md +9 -1
- package/docs/getting-started.md +1 -1
- package/docs/hooks.md +4 -2
- package/docs/runtime/guia-modo-dual-y-migracion.md +44 -3
- package/docs/runtime/protocolo-cliente-runtime.md +10 -0
- package/hooks/build/lib/config.sh +56 -8
- package/hooks/build/lib/harness-meta.sh +72 -0
- package/hooks/build/lib/runtime-client.sh +65 -2
- package/hooks/build/lib/runtime-ops.sh +25 -0
- package/hooks/build/release-ops.sh +25 -4
- package/hooks/build/session-start.sh +5 -61
- package/hooks/build/slice-ops.sh +206 -0
- package/package.json +1 -1
- package/scripts/lib/front-plan.py +55 -6
- 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-worktree-identity.sh +207 -0
- package/skills/managing-parallel-front/SKILL.md +35 -8
|
@@ -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
|
|
|
@@ -135,9 +136,16 @@ HINT
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
# ── front-integration ────────────────────────────────────────────────────────────────
|
|
138
|
-
# Cada worktree se registra como AGENTE PROPIO
|
|
139
|
-
#
|
|
140
|
-
#
|
|
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.
|
|
141
149
|
cmd_front_integration() {
|
|
142
150
|
local front="${1:-}" merge="" resmoke="" ev="" evfile="" mode akey payload resp status
|
|
143
151
|
[ -n "$front" ] || { usage; return $OPS_RC_USAGE; }
|
|
@@ -170,7 +178,10 @@ cmd_front_integration() {
|
|
|
170
178
|
return $OPS_RC_OK
|
|
171
179
|
fi
|
|
172
180
|
akey="$(runtime_field agent_key)"
|
|
173
|
-
[ -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; }
|
|
174
185
|
resp="$(runtime_post "$(ops_path_front_integration "$(ops_urlenc "$front")" "$(ops_urlenc "$akey")")" "$payload")"
|
|
175
186
|
status="$(runtime_http_status)"
|
|
176
187
|
case "$status" in
|
|
@@ -192,6 +203,16 @@ cmd_front_integration() {
|
|
|
192
203
|
|
|
193
204
|
SUB="${1:-}"
|
|
194
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
|
+
|
|
195
216
|
case "$SUB" in
|
|
196
217
|
verdict) cmd_verdict "$@"; exit $? ;;
|
|
197
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=$?
|
package/hooks/build/slice-ops.sh
CHANGED
|
@@ -17,12 +17,20 @@
|
|
|
17
17
|
set -uo pipefail
|
|
18
18
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
19
|
source "$HERE/lib/runtime-ops.sh"
|
|
20
|
+
# [#74] `register` manda el mismo harness_version/asset_types que el arranque de sesión.
|
|
21
|
+
source "$HERE/lib/harness-meta.sh"
|
|
20
22
|
|
|
21
23
|
usage() {
|
|
22
24
|
cat >&2 <<'USAGE'
|
|
23
25
|
uso: slice-ops.sh <subcomando> [opciones]
|
|
24
26
|
|
|
25
27
|
mode imprime legacy|dual|runtime
|
|
28
|
+
register [--runtime-url U] [--force] da de alta ESTE árbol como agente propio con
|
|
29
|
+
un token que el ADMIN emitió para él (el token
|
|
30
|
+
se pide por STDIN, nunca por argumento).
|
|
31
|
+
Obligatorio en un worktree antes de escribir:
|
|
32
|
+
sin él se opera con la identidad heredada del
|
|
33
|
+
clon principal.
|
|
26
34
|
claim reclama trabajo (solo modo runtime; el hub
|
|
27
35
|
reparte por orden de cola — no hay --epic)
|
|
28
36
|
next-step deriva la siguiente acción desde la caché
|
|
@@ -56,6 +64,7 @@ uso: slice-ops.sh <subcomando> [opciones]
|
|
|
56
64
|
escalate «razón» | --file F [--gate G] registra un bloqueo (la decisión es humana)
|
|
57
65
|
|
|
58
66
|
códigos de salida: 0 ok · 2 uso · 3 legacy · 4 sin slice · 5 offline · 6 rechazado · 7 sin trabajo
|
|
67
|
+
· 9 identidad heredada (este worktree no está dado de alta: `register`)
|
|
59
68
|
USAGE
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -1331,6 +1340,16 @@ cmd_status() {
|
|
|
1331
1340
|
esac
|
|
1332
1341
|
done
|
|
1333
1342
|
mode="$(ops_mode)"
|
|
1343
|
+
# [#74] `status` conserva el fallback a propósito — es la herramienta de diagnóstico de un
|
|
1344
|
+
# worktree que todavía no se dio de alta, y negarle la lectura lo dejaría a ciegas. Pero se
|
|
1345
|
+
# dice en la primera línea de quién es el estado que está mirando: sin el banner, un informe
|
|
1346
|
+
# heredado es indistinguible de uno propio, y es exactamente así como se llega a creer que
|
|
1347
|
+
# se tiene un lease que en realidad es de otro agente.
|
|
1348
|
+
if config_is_inherited_root; then
|
|
1349
|
+
echo "identidad: ⚠ HEREDADA del clon principal ($(config_main_clone_root)) — solo lectura"
|
|
1350
|
+
echo " lo de abajo es el estado de ESE agente, no del tuyo. Antes de reclamar"
|
|
1351
|
+
echo " o reportar: \`slice-ops.sh register\` con un token propio del ADMIN."
|
|
1352
|
+
fi
|
|
1334
1353
|
echo "modo: $mode"
|
|
1335
1354
|
if [ "$mode" = "legacy" ]; then
|
|
1336
1355
|
echo "estado: el fichero .claude/state/build-state.json es la fuente de verdad (v0.8.5)"
|
|
@@ -1979,10 +1998,197 @@ cmd_checkpoint() {
|
|
|
1979
1998
|
_ops_post_or_queue "$OPS_PATH_CHECKPOINTS" "$payload" checkpoint_recorded "$sid" "el checkpoint" "$queue_payload"
|
|
1980
1999
|
}
|
|
1981
2000
|
|
|
2001
|
+
# ── register ─────────────────────────────────────────────────────────────────────────
|
|
2002
|
+
# [#74] Alta EXPLÍCITA de este árbol de trabajo como agente propio.
|
|
2003
|
+
#
|
|
2004
|
+
# El caso que lo motiva: dos agentes construyendo dos épicas en la misma máquina, cada uno en
|
|
2005
|
+
# su worktree. El hub reparte las dos sin objeción — el bloqueo estaba aquí: un worktree sin
|
|
2006
|
+
# `.claude/state/runtime.credentials` propio hereda el del clon principal por el fallback de
|
|
2007
|
+
# `config_root()` y trabaja con SU identidad sin decirlo.
|
|
2008
|
+
#
|
|
2009
|
+
# Tres decisiones de diseño que no son gratuitas:
|
|
2010
|
+
# 1. El token entra por STDIN (prompt si hay TTY), jamás por argumento: un `--token` queda
|
|
2011
|
+
# en el historial del shell y en `ps`. Por eso `register` es un comando de terminal
|
|
2012
|
+
# humano y NO se invoca desde dentro de un claim — los hooks y las tool calls no tienen
|
|
2013
|
+
# TTY y un `read` ahí colgaría la llamada.
|
|
2014
|
+
# 2. Se siembra PRIMERO el fichero local, para que `config_root()` ancle en este árbol antes
|
|
2015
|
+
# de cualquier merge. Al revés, `runtime_register` habría hecho upsert del token nuevo
|
|
2016
|
+
# sobre el fichero del clon principal, pisándole la identidad al agente que trabaja ahí.
|
|
2017
|
+
# 3. Se valida que el `agent_key` obtenido DIFIERA del que tiene el principal. El hub no crea
|
|
2018
|
+
# identidades en `/agents/register`: el `agent_key` es el `agent_id` que el ADMIN grabó en
|
|
2019
|
+
# el token al emitirlo. Mismo token ⇒ mismo agente ⇒ el claim devolvería el trabajo del
|
|
2020
|
+
# otro worktree. Un token por agente, siempre.
|
|
2021
|
+
#
|
|
2022
|
+
# Ante cualquier fallo se revierte: o el árbol queda con identidad propia y verificada, o
|
|
2023
|
+
# queda exactamente como estaba. Un fichero de credenciales a medias es peor que ninguno.
|
|
2024
|
+
|
|
2025
|
+
# _reg_rollback <fichero> <backup> — deja el fichero local como estaba antes de `register`.
|
|
2026
|
+
_reg_rollback() {
|
|
2027
|
+
local file="$1" backup="$2"
|
|
2028
|
+
if [ -n "$backup" ] && [ -f "$backup" ]; then
|
|
2029
|
+
cp "$backup" "$file" 2>/dev/null
|
|
2030
|
+
rm -f "$backup"
|
|
2031
|
+
else
|
|
2032
|
+
rm -f "$file"
|
|
2033
|
+
fi
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
cmd_register() {
|
|
2037
|
+
local url="" force=0 wt main localfile mainfile mainkey newkey token backup="" hv at rc
|
|
2038
|
+
while [ $# -gt 0 ]; do
|
|
2039
|
+
case "$1" in
|
|
2040
|
+
--runtime-url) url="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
2041
|
+
--force) force=1; shift ;;
|
|
2042
|
+
--token|--project-token|--runtime-token)
|
|
2043
|
+
echo "⛔ el token NO se pasa por argumento: quedaría en el historial del shell y en \`ps\`." >&2
|
|
2044
|
+
echo " \`register\` lo pide por stdin. Córrelo en un terminal humano." >&2
|
|
2045
|
+
return $OPS_RC_USAGE ;;
|
|
2046
|
+
*) shift ;;
|
|
2047
|
+
esac
|
|
2048
|
+
done
|
|
2049
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
2050
|
+
echo "⛔ register necesita python3 para escribir las credenciales." >&2; return $OPS_RC_OFFLINE; }
|
|
2051
|
+
|
|
2052
|
+
wt="$(config_candidate_root)"
|
|
2053
|
+
main="$(config_main_clone_root)"
|
|
2054
|
+
localfile="$wt/.claude/state/runtime.credentials"
|
|
2055
|
+
mainfile=""
|
|
2056
|
+
if [ -n "$main" ] && [ "$main" != "$wt" ]; then mainfile="$main/.claude/state/runtime.credentials"; fi
|
|
2057
|
+
|
|
2058
|
+
if [ -f "$localfile" ] && [ "$force" != 1 ]; then
|
|
2059
|
+
echo "este árbol YA tiene identidad propia — no se toca nada:"
|
|
2060
|
+
echo " raíz: $wt"
|
|
2061
|
+
echo " agente: $(runtime_credentials_field_at "$localfile" agent_key)"
|
|
2062
|
+
echo " proyecto: $(runtime_credentials_field_at "$localfile" project_id)"
|
|
2063
|
+
echo "(para re-registrarlo con OTRO token: \`register --force\`)"
|
|
2064
|
+
return $OPS_RC_OK
|
|
2065
|
+
fi
|
|
2066
|
+
|
|
2067
|
+
# La URL no es secreta y es la MISMA del proyecto: se hereda del principal si no se da.
|
|
2068
|
+
if [ -z "$url" ]; then
|
|
2069
|
+
url="$(runtime_credentials_field_at "$localfile" runtime_url)"
|
|
2070
|
+
[ -n "$url" ] || url="$(runtime_credentials_field_at "$mainfile" runtime_url)"
|
|
2071
|
+
fi
|
|
2072
|
+
case "$url" in
|
|
2073
|
+
http://*|https://*) : ;;
|
|
2074
|
+
*)
|
|
2075
|
+
echo "⛔ no hay runtime_url que usar (ni --runtime-url, ni credenciales del clon principal)." >&2
|
|
2076
|
+
echo " Pásala explícita: \`register --runtime-url https://…\`" >&2
|
|
2077
|
+
return $OPS_RC_USAGE ;;
|
|
2078
|
+
esac
|
|
2079
|
+
|
|
2080
|
+
mainkey="$(runtime_credentials_field_at "$mainfile" agent_key)"
|
|
2081
|
+
|
|
2082
|
+
if [ -t 0 ]; then
|
|
2083
|
+
printf 'Token de agente para este árbol (no se muestra en pantalla): ' >&2
|
|
2084
|
+
IFS= read -rs token
|
|
2085
|
+
printf '\n' >&2
|
|
2086
|
+
else
|
|
2087
|
+
IFS= read -r token || token=""
|
|
2088
|
+
fi
|
|
2089
|
+
token="$(printf '%s' "$token" | tr -d '[:space:]')"
|
|
2090
|
+
[ -n "$token" ] || { echo "⛔ token vacío: no se registró nada." >&2; return $OPS_RC_USAGE; }
|
|
2091
|
+
|
|
2092
|
+
# Compuerta barata y decisiva, ANTES de la red: si el token es el del clon principal, el hub
|
|
2093
|
+
# devolverá su misma identidad — el `agent_key` va grabado en el token, no se crea al
|
|
2094
|
+
# registrar. Preguntarlo por HTTP solo añadiría una llamada al mismo veredicto.
|
|
2095
|
+
if [ -n "$mainfile" ] && [ "$token" = "$(runtime_credentials_field_at "$mainfile" project_token)" ]; then
|
|
2096
|
+
token=""
|
|
2097
|
+
echo "⛔ ese token es la identidad del clon principal: registrarlo aquí daría el MISMO" >&2
|
|
2098
|
+
echo " agent_key y el claim te devolvería el trabajo que ya tiene reclamado el otro árbol." >&2
|
|
2099
|
+
echo " Pide al ADMIN un token con \`agent_id\` distinto (uno por agente)." >&2
|
|
2100
|
+
echo " El fichero del clon principal NO se ha tocado." >&2
|
|
2101
|
+
return $OPS_RC_REJECTED
|
|
2102
|
+
fi
|
|
2103
|
+
|
|
2104
|
+
if [ -f "$localfile" ]; then
|
|
2105
|
+
backup="$(mktemp)" && cp "$localfile" "$backup" 2>/dev/null || backup=""
|
|
2106
|
+
fi
|
|
2107
|
+
|
|
2108
|
+
if ! TRYCORE_REGISTER_TOKEN="$token" runtime_credentials_seed_at "$localfile" "$url"; then
|
|
2109
|
+
token=""
|
|
2110
|
+
_reg_rollback "$localfile" "$backup"
|
|
2111
|
+
echo "⛔ no se pudo escribir $localfile — revisa permisos." >&2
|
|
2112
|
+
return $OPS_RC_OFFLINE
|
|
2113
|
+
fi
|
|
2114
|
+
token="" # el secreto ya está en el fichero 0600: fuera de las variables del shell
|
|
2115
|
+
|
|
2116
|
+
hv="$(harness_version)"
|
|
2117
|
+
at="$(harness_asset_types)"
|
|
2118
|
+
runtime_register "$hv" "$at"
|
|
2119
|
+
rc=$?
|
|
2120
|
+
if [ "$rc" -ne 0 ]; then
|
|
2121
|
+
_reg_rollback "$localfile" "$backup"
|
|
2122
|
+
if [ "$rc" -eq 2 ]; then
|
|
2123
|
+
echo "⛔ el hub rechaza esta versión del arnés ($hv): incompatible_version." >&2
|
|
2124
|
+
echo " Actualiza el paquete y repite \`register\`." >&2
|
|
2125
|
+
return $OPS_RC_REJECTED
|
|
2126
|
+
fi
|
|
2127
|
+
case "$(runtime_http_status)" in
|
|
2128
|
+
401|403)
|
|
2129
|
+
echo "⛔ el hub no acepta ese token (HTTP $(runtime_http_status)): pide al ADMIN uno válido" >&2
|
|
2130
|
+
echo " para este proyecto. Nada quedó escrito." >&2
|
|
2131
|
+
return $OPS_RC_REJECTED ;;
|
|
2132
|
+
*)
|
|
2133
|
+
echo "OFFLINE: no se pudo registrar contra $url (HTTP $(runtime_http_status))." >&2
|
|
2134
|
+
echo " Nada quedó escrito: repite \`register\` cuando haya red." >&2
|
|
2135
|
+
return $OPS_RC_OFFLINE ;;
|
|
2136
|
+
esac
|
|
2137
|
+
fi
|
|
2138
|
+
|
|
2139
|
+
newkey="$(runtime_credentials_field_at "$localfile" agent_key)"
|
|
2140
|
+
if [ -z "$newkey" ]; then
|
|
2141
|
+
_reg_rollback "$localfile" "$backup"
|
|
2142
|
+
echo "⛔ el hub respondió 200 pero sin \`agent_key\`: sin identidad verificable no se da" >&2
|
|
2143
|
+
echo " de alta el árbol. Nada quedó escrito." >&2
|
|
2144
|
+
return $OPS_RC_REJECTED
|
|
2145
|
+
fi
|
|
2146
|
+
if [ -n "$mainkey" ] && [ "$newkey" = "$mainkey" ]; then
|
|
2147
|
+
_reg_rollback "$localfile" "$backup"
|
|
2148
|
+
echo "⛔ ese token resuelve al MISMO agent_key que el clon principal ($mainkey): no es una" >&2
|
|
2149
|
+
echo " identidad nueva. El \`agent_key\` lo graba el ADMIN en el token al emitirlo —" >&2
|
|
2150
|
+
echo " pide uno con \`agent_id\` distinto. El fichero del clon principal NO se ha tocado." >&2
|
|
2151
|
+
return $OPS_RC_REJECTED
|
|
2152
|
+
fi
|
|
2153
|
+
rm -f "$backup"
|
|
2154
|
+
|
|
2155
|
+
# A partir de aquí `config_root()` ancla en este árbol, y con él TODA la resolución de
|
|
2156
|
+
# config: si `build-config.json` no viajó al worktree (no está versionado en el consumidor),
|
|
2157
|
+
# `runtime.mode` caería al default `legacy` y el árbol recién dado de alta dejaría de hablar
|
|
2158
|
+
# con el hub — el registro habría «funcionado» apagando justo lo que venía a encender. Se
|
|
2159
|
+
# copia del principal solo si aquí no hay ninguno: nunca se pisa una config local.
|
|
2160
|
+
if [ -n "$main" ] && [ ! -f "$wt/.claude/config/build-config.json" ] \
|
|
2161
|
+
&& [ -f "$main/.claude/config/build-config.json" ]; then
|
|
2162
|
+
mkdir -p "$wt/.claude/config"
|
|
2163
|
+
cp "$main/.claude/config/build-config.json" "$wt/.claude/config/build-config.json" 2>/dev/null \
|
|
2164
|
+
&& echo " (build-config.json copiado del clon principal: modo $(ops_mode))"
|
|
2165
|
+
fi
|
|
2166
|
+
|
|
2167
|
+
echo "árbol dado de alta con identidad propia:"
|
|
2168
|
+
echo " raíz: $wt"
|
|
2169
|
+
echo " agente: $newkey"
|
|
2170
|
+
echo " proyecto: $(runtime_credentials_field_at "$localfile" project_id)"
|
|
2171
|
+
[ -n "$mainkey" ] && echo " (distinto del clon principal: $mainkey — su fichero no se tocó)"
|
|
2172
|
+
echo "Desde aquí el claim, los gates y el heartbeat son de ESTE árbol. Siguiente paso:"
|
|
2173
|
+
echo " bash .claude/hooks/build/slice-ops.sh claim"
|
|
2174
|
+
return $OPS_RC_OK
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1982
2177
|
SUB="${1:-}"
|
|
1983
2178
|
[ $# -gt 0 ] && shift
|
|
2179
|
+
|
|
2180
|
+
# [#74] Compuerta de identidad ANTES de despachar. Las operaciones de ESCRITURA — todo lo que
|
|
2181
|
+
# firma un acto con el `agent_key` o toca la cola/los ledgers — abortan si este árbol heredó la
|
|
2182
|
+
# identidad del clon principal. Las LECTURAS (`mode`, `next-step`, `status`, los `*-status`)
|
|
2183
|
+
# quedan fuera a propósito: son el diagnóstico de un worktree que aún no se dio de alta, y
|
|
2184
|
+
# `status` avisa de quién es el estado que enseña. `register` tampoco entra: es la salida.
|
|
2185
|
+
case " claim phase gate submit archive wiring progress checkpoint fact propose-asset propose-epic epic-writeback graph-sync escalate " in
|
|
2186
|
+
*" $SUB "*) ops_guard_own_identity "$SUB" || exit $? ;;
|
|
2187
|
+
esac
|
|
2188
|
+
|
|
1984
2189
|
case "$SUB" in
|
|
1985
2190
|
mode) ops_mode; exit 0 ;;
|
|
2191
|
+
register) cmd_register "$@"; exit $? ;;
|
|
1986
2192
|
claim) cmd_claim "$@"; exit $? ;;
|
|
1987
2193
|
next-step) cmd_next_step; exit $? ;;
|
|
1988
2194
|
phase) cmd_phase "$@"; exit $? ;;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycore/spec-build-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Arnés agéntico de construcción de Trycore para Claude Code: pipeline de dos loops (slice por épica + release gate) con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
(conjunto disjunto + merge_order) que un humano aprueba al abrir el front en la consola del
|
|
6
6
|
hub. No llama al runtime ni lee el estado: entra JSON por stdin, sale JSON por stdout.
|
|
7
7
|
|
|
8
|
-
stdin: JSON [{"epica","layer","files_scope":[glob]}]
|
|
8
|
+
stdin: JSON [{"epica","layer","files_scope":[glob],"depends_on":[EP-XXX]}]
|
|
9
9
|
stdout: {"selected":[...],"serialized":[{"epica","reason"}],"excluded_foundational":[...]}
|
|
10
10
|
Determinista: orden por 'epica'. Solape de globs por prefijo de directorio o glob idéntico.
|
|
11
|
+
|
|
12
|
+
Las tres compuertas, en orden (METODOLOGIA §5 «Compuertas del front»):
|
|
13
|
+
G1 `layer` fundacional -> excluded_foundational (nunca en paralelo).
|
|
14
|
+
G2a `files_scope` vacío o ausente -> serialized. «Sin `files_scope` declarado, una épica no
|
|
15
|
+
es candidata al front» (METODOLOGIA:343): fail-CLOSED. Que dos épicas sin ámbito
|
|
16
|
+
declarado salieran «disjuntas» era exactamente el fallo que el front no puede permitirse.
|
|
17
|
+
G2b camino en el DAG de `depends_on` con una épica ya seleccionada -> serialized. Dos épicas
|
|
18
|
+
encadenadas no son paralelizables aunque sus ficheros no se toquen: la de abajo construye
|
|
19
|
+
sobre lo que la de arriba todavía no ha mergeado.
|
|
20
|
+
G2c solape de `files_scope` con una épica ya seleccionada -> serialized.
|
|
11
21
|
"""
|
|
12
22
|
import json, sys, fnmatch
|
|
13
23
|
|
|
@@ -24,6 +34,37 @@ def overlap(a, b):
|
|
|
24
34
|
return True
|
|
25
35
|
return False
|
|
26
36
|
|
|
37
|
+
def is_foundational(cand):
|
|
38
|
+
"""G1 normalizada. El bundle del grafo emite `layer` en MAYÚSCULA (graph-bundle.py lo
|
|
39
|
+
`.upper()`a porque el hub lo exige así) mientras METODOLOGIA lo escribe en minúscula: un
|
|
40
|
+
`== "foundational"` a secas dejaba pasar `FOUNDATIONAL` al front, en silencio y justo en la
|
|
41
|
+
compuerta que más caro sale saltarse."""
|
|
42
|
+
layer = cand.get("layer")
|
|
43
|
+
return isinstance(layer, str) and layer.strip().lower() == "foundational"
|
|
44
|
+
|
|
45
|
+
def reachability(cands):
|
|
46
|
+
"""Alcance transitivo de `depends_on` entre las candidatas (clausura por Floyd-Warshall
|
|
47
|
+
booleano; los conjuntos de épicas de un front son de decenas, no de miles).
|
|
48
|
+
|
|
49
|
+
Se calcula sobre TODAS las candidatas, fundacionales incluidas: un camino EP-A → EP-F → EP-B
|
|
50
|
+
encadena A y B aunque la fundacional del medio no entre al front. Las referencias a épicas
|
|
51
|
+
que no vienen en la entrada se ignoran — aquí no se inventa un grafo que no se ha visto."""
|
|
52
|
+
codes = [c.get("epica") for c in cands if isinstance(c.get("epica"), str)]
|
|
53
|
+
known = set(codes)
|
|
54
|
+
reach = {c: set() for c in codes}
|
|
55
|
+
for c in cands:
|
|
56
|
+
code = c.get("epica")
|
|
57
|
+
if code not in known:
|
|
58
|
+
continue
|
|
59
|
+
for dep in c.get("depends_on") or []:
|
|
60
|
+
if isinstance(dep, str) and dep in known and dep != code:
|
|
61
|
+
reach[code].add(dep)
|
|
62
|
+
for k in codes:
|
|
63
|
+
for i in codes:
|
|
64
|
+
if k in reach[i]:
|
|
65
|
+
reach[i] |= reach[k]
|
|
66
|
+
return reach
|
|
67
|
+
|
|
27
68
|
def _empty_result():
|
|
28
69
|
return {"selected": [], "serialized": [], "excluded_foundational": []}
|
|
29
70
|
|
|
@@ -33,15 +74,23 @@ def main():
|
|
|
33
74
|
if not isinstance(cands, list) or not all(isinstance(c, dict) for c in cands):
|
|
34
75
|
raise ValueError("entrada inválida: se esperaba una lista de objetos")
|
|
35
76
|
cands = sorted(cands, key=lambda c: c.get("epica",""))
|
|
36
|
-
|
|
37
|
-
|
|
77
|
+
reach = reachability(cands)
|
|
78
|
+
excluded = [c["epica"] for c in cands if is_foundational(c)]
|
|
79
|
+
pool = [c for c in cands if not is_foundational(c)]
|
|
38
80
|
selected, sel_scopes, serialized = [], [], []
|
|
39
81
|
for c in pool:
|
|
82
|
+
code = c["epica"]
|
|
40
83
|
sc = c.get("files_scope") or []
|
|
41
|
-
if
|
|
42
|
-
serialized.append({"epica":
|
|
84
|
+
if not sc:
|
|
85
|
+
serialized.append({"epica": code,
|
|
86
|
+
"reason": "sin files_scope declarado: no es candidata al front (METODOLOGIA §5 G2)"})
|
|
87
|
+
elif any(code in reach.get(s, ()) or s in reach.get(code, ()) for s in selected):
|
|
88
|
+
serialized.append({"epica": code,
|
|
89
|
+
"reason": "dependencia en el grafo con una épica ya seleccionada"})
|
|
90
|
+
elif any(overlap(sc, s) for s in sel_scopes):
|
|
91
|
+
serialized.append({"epica": code, "reason": "solape de files_scope con épica seleccionada"})
|
|
43
92
|
else:
|
|
44
|
-
selected.append(
|
|
93
|
+
selected.append(code); sel_scopes.append(sc)
|
|
45
94
|
print(json.dumps({"selected":selected,"serialized":serialized,"excluded_foundational":excluded}))
|
|
46
95
|
except Exception:
|
|
47
96
|
print(json.dumps(_empty_result()))
|
package/scripts/smoke-test.sh
CHANGED
|
@@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
5
5
|
FAILED=0
|
|
6
6
|
|
|
7
7
|
echo "── Tests del motor de contexto y front ──"
|
|
8
|
-
for t in test-schema.sh test-config.sh test-runtime-client.sh test-hooks-runtime.sh test-skill-ops.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
|
|
8
|
+
for t in test-schema.sh test-config.sh test-runtime-client.sh test-hooks-runtime.sh test-skill-ops.sh test-worktree-identity.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
|
|
9
9
|
if bash "$DIR/tests/$t"; then echo "smoke: $t OK"; else echo "smoke: $t FALLÓ"; FAILED=1; fi
|
|
10
10
|
done
|
|
11
11
|
|
|
@@ -63,4 +63,44 @@ got="$(cd "$BARE" && CLAUDE_PROJECT_DIR="$BARE" config_root)"
|
|
|
63
63
|
[ "$got" = "$BARE" ] && echo "OK config_root sin estado devuelve el candidato" \
|
|
64
64
|
|| { echo "FAIL config_root sin estado ($got)"; fail=1; }
|
|
65
65
|
|
|
66
|
+
# ── [#74] Señal de identidad heredada ─────────────────────────────────────────
|
|
67
|
+
# El fallback de arriba es correcto para LEER y veneno para ESCRIBIR: sin una señal
|
|
68
|
+
# que lo distinga, un worktree sin credenciales propias firma sus actos con la
|
|
69
|
+
# identidad del clon principal y nadie se entera. Estos tres helpers son esa señal.
|
|
70
|
+
|
|
71
|
+
got="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" config_candidate_root)"
|
|
72
|
+
WTP="$(cd "$WT" && pwd -P)"
|
|
73
|
+
[ "$got" = "$WTP" ] && echo "OK config_candidate_root devuelve ESTE árbol, sin fallback" \
|
|
74
|
+
|| { echo "FAIL config_candidate_root ($got, esperado $WTP)"; fail=1; }
|
|
75
|
+
|
|
76
|
+
got="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" config_main_clone_root)"
|
|
77
|
+
[ "$got" = "$MAIN" ] && echo "OK config_main_clone_root ve el clon principal desde el worktree" \
|
|
78
|
+
|| { echo "FAIL config_main_clone_root ($got, esperado $MAIN)"; fail=1; }
|
|
79
|
+
|
|
80
|
+
(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" config_is_inherited_root) \
|
|
81
|
+
&& echo "OK config_is_inherited_root detecta el worktree sin credenciales propias" \
|
|
82
|
+
|| { echo "FAIL config_is_inherited_root no detecta la herencia"; fail=1; }
|
|
83
|
+
|
|
84
|
+
# El clon principal NUNCA hereda: es el dueño del estado.
|
|
85
|
+
(cd "$MAIN" && CLAUDE_PROJECT_DIR="$MAIN" config_is_inherited_root) \
|
|
86
|
+
&& { echo "FAIL config_is_inherited_root marca herencia en el clon principal"; fail=1; } \
|
|
87
|
+
|| echo "OK config_is_inherited_root no marca al clon principal"
|
|
88
|
+
|
|
89
|
+
# Un proyecto sin arnés tampoco: no hay identidad que heredar.
|
|
90
|
+
(cd "$BARE" && CLAUDE_PROJECT_DIR="$BARE" config_is_inherited_root) \
|
|
91
|
+
&& { echo "FAIL config_is_inherited_root marca herencia sin estado en ninguna parte"; fail=1; } \
|
|
92
|
+
|| echo "OK config_is_inherited_root no marca un proyecto sin arnés"
|
|
93
|
+
|
|
94
|
+
# Y en cuanto el worktree tiene credenciales PROPIAS, deja de heredar — sin tocar nada
|
|
95
|
+
# más: es exactamente lo que `slice-ops.sh register` deja hecho.
|
|
96
|
+
mkdir -p "$WT/.claude/state"
|
|
97
|
+
echo '{"runtime_url":"http://localhost","agent_key":"ak-wt"}' > "$WT/.claude/state/runtime.credentials"
|
|
98
|
+
(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" config_is_inherited_root) \
|
|
99
|
+
&& { echo "FAIL sigue heredando con credenciales propias"; fail=1; } \
|
|
100
|
+
|| echo "OK con credenciales propias el worktree deja de heredar"
|
|
101
|
+
got="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" config_root)"
|
|
102
|
+
[ "$got" = "$WT" ] && echo "OK config_root ancla en el worktree ya registrado" \
|
|
103
|
+
|| { echo "FAIL config_root tras registrar ($got, esperado $WT)"; fail=1; }
|
|
104
|
+
rm -f "$WT/.claude/state/runtime.credentials"
|
|
105
|
+
|
|
66
106
|
exit $fail
|
|
@@ -25,6 +25,59 @@ echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
|
25
25
|
assert d['selected']==['EP-010'] and [m['epica'] for m in d['serialized']]==['EP-011'], d
|
|
26
26
|
print('OK front-plan mid-wildcard overlap serializado')" || { echo "FAIL mid-wildcard overlap"; fail=1; }
|
|
27
27
|
|
|
28
|
+
# ── [#74] G1 normalizada: el bundle del grafo emite `layer` en MAYÚSCULA ─────────────
|
|
29
|
+
# graph-bundle.py hace `.upper()` porque el hub lo exige así; front-plan comparaba contra
|
|
30
|
+
# "foundational" en minúscula. La exclusión G1 no disparaba, en silencio, justo en la
|
|
31
|
+
# compuerta que más caro sale saltarse.
|
|
32
|
+
out="$(run '[{"epica":"EP-001","layer":"FOUNDATIONAL","files_scope":["src/**"]},
|
|
33
|
+
{"epica":"EP-002","layer":"BUSINESS","files_scope":["src/a/**"]}]')"
|
|
34
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
35
|
+
assert d['excluded_foundational']==['EP-001'], d
|
|
36
|
+
assert d['selected']==['EP-002'], d
|
|
37
|
+
print('OK front-plan G1 con layer en MAYÚSCULA')" || { echo "FAIL G1 mayúscula"; fail=1; }
|
|
38
|
+
|
|
39
|
+
# ── [#74] G2 fail-CLOSED sin files_scope ─────────────────────────────────────────────
|
|
40
|
+
# METODOLOGIA:343: «sin `files_scope` declarado, una épica no es candidata al front». El
|
|
41
|
+
# código hacía lo contrario: con la lista vacía, `overlap([], …)` es falso y dos épicas sin
|
|
42
|
+
# ámbito salían como disjuntas.
|
|
43
|
+
out="$(run '[{"epica":"EP-020","layer":"business","files_scope":[]},
|
|
44
|
+
{"epica":"EP-021","layer":"business"},
|
|
45
|
+
{"epica":"EP-022","layer":"business","files_scope":["src/c/**"]}]')"
|
|
46
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
47
|
+
assert d['selected']==['EP-022'], d
|
|
48
|
+
assert {m['epica'] for m in d['serialized']}=={'EP-020','EP-021'}, d
|
|
49
|
+
assert all('files_scope' in m['reason'] for m in d['serialized']), d
|
|
50
|
+
print('OK front-plan sin files_scope -> serialized (fail-closed)')" || { echo "FAIL fail-closed sin files_scope"; fail=1; }
|
|
51
|
+
|
|
52
|
+
# ── [#74] G2 por camino en el DAG de `depends_on` ────────────────────────────────────
|
|
53
|
+
# Ficheros disjuntos no bastan: si EP-031 depende de EP-030, la de abajo construye sobre lo
|
|
54
|
+
# que la de arriba todavía no ha mergeado. La transitividad cuenta, y el camino vale aunque
|
|
55
|
+
# pase por una fundacional que no entra al front.
|
|
56
|
+
out="$(run '[{"epica":"EP-030","layer":"business","files_scope":["src/x/**"]},
|
|
57
|
+
{"epica":"EP-031","layer":"business","files_scope":["src/y/**"],"depends_on":["EP-030"]},
|
|
58
|
+
{"epica":"EP-032","layer":"business","files_scope":["src/z/**"],"depends_on":["EP-031"]},
|
|
59
|
+
{"epica":"EP-033","layer":"business","files_scope":["src/w/**"],"depends_on":["EP-099"]}]')"
|
|
60
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
61
|
+
assert d['selected']==['EP-030','EP-033'], d
|
|
62
|
+
assert {m['epica'] for m in d['serialized']}=={'EP-031','EP-032'}, d
|
|
63
|
+
assert all('grafo' in m['reason'] for m in d['serialized']), d
|
|
64
|
+
print('OK front-plan serializa el camino transitivo del DAG')" || { echo "FAIL DAG depends_on"; fail=1; }
|
|
65
|
+
|
|
66
|
+
out="$(run '[{"epica":"EP-040","layer":"business","files_scope":["src/x/**"]},
|
|
67
|
+
{"epica":"EP-041","layer":"FOUNDATIONAL","files_scope":["src/f/**"],"depends_on":["EP-040"]},
|
|
68
|
+
{"epica":"EP-042","layer":"business","files_scope":["src/z/**"],"depends_on":["EP-041"]}]')"
|
|
69
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
70
|
+
assert d['selected']==['EP-040'], d
|
|
71
|
+
assert [m['epica'] for m in d['serialized']]==['EP-042'], d
|
|
72
|
+
print('OK front-plan ve el camino que pasa por una fundacional excluida')" || { echo "FAIL DAG a través de fundacional"; fail=1; }
|
|
73
|
+
|
|
74
|
+
# Sin `depends_on` declarado nada cambia: el campo es opcional, no se inventa un grafo.
|
|
75
|
+
out="$(run '[{"epica":"EP-050","layer":"business","files_scope":["src/p/**"]},
|
|
76
|
+
{"epica":"EP-051","layer":"business","files_scope":["src/q/**"]}]')"
|
|
77
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
78
|
+
assert d['selected']==['EP-050','EP-051'] and d['serialized']==[], d
|
|
79
|
+
print('OK front-plan sin depends_on no serializa de más')" || { echo "FAIL sin depends_on"; fail=1; }
|
|
80
|
+
|
|
28
81
|
SK="$ROOT/skills/managing-parallel-front/SKILL.md"
|
|
29
82
|
{ [ -f "$SK" ] && grep -q 'git worktree add' "$SK" && grep -q 'front-plan.py' "$SK" \
|
|
30
83
|
&& grep -q 'merge_order' "$SK" && grep -q 'foundational' "$SK"; } \
|
|
@@ -35,4 +88,14 @@ SK="$ROOT/skills/managing-parallel-front/SKILL.md"
|
|
|
35
88
|
grep -q 'files_scope' "$ROOT/skills/building-a-slice/references/dor.md" && echo "OK dor files_scope" || { echo "FAIL dor files_scope"; fail=1; }
|
|
36
89
|
grep -q 'parallel_front' "$ROOT/agents/build/build-orchestrator.md" && echo "OK orquestador front" || { echo "FAIL orquestador front"; fail=1; }
|
|
37
90
|
|
|
91
|
+
# [#74] La skill decía «mismo token de proyecto»: el hub lo refuta (el `agent_key` es el
|
|
92
|
+
# `agent_id` grabado en el token por el ADMIN). Un token repetido = un solo agente.
|
|
93
|
+
grep -q 'mismo token de proyecto' "$SK" \
|
|
94
|
+
&& { echo "FAIL la skill sigue diciendo «mismo token de proyecto»"; fail=1; } \
|
|
95
|
+
|| echo "OK la skill ya no manda reusar el token de proyecto"
|
|
96
|
+
grep -q 'slice-ops.sh register' "$SK" && echo "OK la skill manda dar de alta el worktree con register" \
|
|
97
|
+
|| { echo "FAIL la skill no menciona register"; fail=1; }
|
|
98
|
+
grep -q 'depends_on' "$SK" && echo "OK la skill pide depends_on entre las candidatas" \
|
|
99
|
+
|| { echo "FAIL la skill no pide depends_on"; fail=1; }
|
|
100
|
+
|
|
38
101
|
exit $fail
|