@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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dual-compare.sh — comparador dual (hook Stop) [EP-OR-08-E].
|
|
3
|
+
# En modo dual el fichero es primario y cada transición se espeja al runtime
|
|
4
|
+
# (protocolo-cliente-runtime §10: POST …/mirror/transitions). Este comparador detecta
|
|
5
|
+
# cuando el REDUCER del servidor llegó a una proyección distinta de lo que dice el
|
|
6
|
+
# fichero local — señal de un espejo rechazado o perdido. Discrepancia = issue
|
|
7
|
+
# bloqueante DEL CORTE (spec riesgos §4: "Divergencia dual… comparador automático en el
|
|
8
|
+
# hook Stop; discrepancia = issue bloqueante del corte") — nunca bloquea el trabajo
|
|
9
|
+
# local, se escala para revisión humana vía el mismo canal que /build:escalate.
|
|
10
|
+
# Fail-open en TODO: sin dual, sin python3, sin build-state.json o con JSON corrupto,
|
|
11
|
+
# sale 0 sin escalar nada.
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
source "$HERE/lib/runtime-client.sh"
|
|
15
|
+
source "$HERE/lib/agent-context.sh"
|
|
16
|
+
|
|
17
|
+
MODE="$(runtime_mode)"
|
|
18
|
+
[ "$MODE" = "dual" ] || exit 0
|
|
19
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
20
|
+
|
|
21
|
+
ROOT="$(config_root)"
|
|
22
|
+
STATE_FILE="$ROOT/.claude/state/build-state.json"
|
|
23
|
+
[ -f "$STATE_FILE" ] || exit 0
|
|
24
|
+
|
|
25
|
+
# Refresco best-effort ANTES de comparar: Stop no es sensible a latencia como
|
|
26
|
+
# PreToolUse (protocolo §4.2-B), y sin refresco fresco una discrepancia real podría
|
|
27
|
+
# quedar enmascarada por una caché vieja de esta misma sesión.
|
|
28
|
+
agent_context_fetch_and_cache >/dev/null 2>&1 || true
|
|
29
|
+
|
|
30
|
+
FILE_SLICE="$(python3 - "$STATE_FILE" <<'PY' 2>/dev/null
|
|
31
|
+
import json,sys
|
|
32
|
+
try:
|
|
33
|
+
st = json.load(open(sys.argv[1]))
|
|
34
|
+
except Exception:
|
|
35
|
+
print("null")
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
sl = st.get("active_slice")
|
|
38
|
+
if not isinstance(sl, dict):
|
|
39
|
+
print("null")
|
|
40
|
+
else:
|
|
41
|
+
print(json.dumps({
|
|
42
|
+
"epica": sl.get("epica"),
|
|
43
|
+
"phase": sl.get("phase"),
|
|
44
|
+
"gates": sl.get("gates") if isinstance(sl.get("gates"), dict) else {},
|
|
45
|
+
}))
|
|
46
|
+
PY
|
|
47
|
+
)"
|
|
48
|
+
[ -n "$FILE_SLICE" ] || exit 0
|
|
49
|
+
|
|
50
|
+
PROJ="$(runtime_projection_read)"
|
|
51
|
+
|
|
52
|
+
REASONS_JSON="$(python3 - "$FILE_SLICE" "$PROJ" <<'PY' 2>/dev/null
|
|
53
|
+
import json,sys
|
|
54
|
+
try:
|
|
55
|
+
file_slice = json.loads(sys.argv[1])
|
|
56
|
+
proj = json.loads(sys.argv[2]) if sys.argv[2] else {}
|
|
57
|
+
except Exception:
|
|
58
|
+
print("[]")
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
proj_slice = proj.get("active_slice") if isinstance(proj, dict) else None
|
|
61
|
+
|
|
62
|
+
reasons = []
|
|
63
|
+
if file_slice and not proj_slice:
|
|
64
|
+
reasons.append("el fichero tiene un slice activo (%s) pero el runtime no reporta ninguno" % file_slice.get("epica"))
|
|
65
|
+
elif proj_slice and not file_slice:
|
|
66
|
+
reasons.append("el runtime reporta un slice activo (%s) pero el fichero no tiene ninguno" % proj_slice.get("epic_code"))
|
|
67
|
+
elif file_slice and proj_slice:
|
|
68
|
+
if file_slice.get("epica") != proj_slice.get("epic_code"):
|
|
69
|
+
reasons.append("épica distinta: fichero=%s runtime=%s" % (file_slice.get("epica"), proj_slice.get("epic_code")))
|
|
70
|
+
if file_slice.get("phase") != proj_slice.get("phase"):
|
|
71
|
+
reasons.append("fase distinta: fichero=%s runtime=%s" % (file_slice.get("phase"), proj_slice.get("phase")))
|
|
72
|
+
fg = file_slice.get("gates") or {}
|
|
73
|
+
pg = proj_slice.get("gates") or {}
|
|
74
|
+
for k in sorted(set(fg) & set(pg)):
|
|
75
|
+
fv, pv = fg.get(k), pg.get(k)
|
|
76
|
+
# Solo gates YA resueltos en ambos lados (True/False); null=pendiente no cuenta.
|
|
77
|
+
if fv is not None and pv is not None and fv != pv:
|
|
78
|
+
reasons.append("gate %s distinto: fichero=%s runtime=%s" % (k, fv, pv))
|
|
79
|
+
|
|
80
|
+
print(json.dumps(reasons, ensure_ascii=False))
|
|
81
|
+
PY
|
|
82
|
+
)"
|
|
83
|
+
[ -n "$REASONS_JSON" ] && [ "$REASONS_JSON" != "[]" ] || exit 0
|
|
84
|
+
|
|
85
|
+
REASON_TEXT="$(python3 -c "import json,sys; print('; '.join(json.loads(sys.argv[1])))" "$REASONS_JSON" 2>/dev/null)"
|
|
86
|
+
[ -n "$REASON_TEXT" ] || exit 0
|
|
87
|
+
|
|
88
|
+
echo "⚠ dual: discrepancia entre el fichero local y la proyección del runtime — ${REASON_TEXT}" >&2
|
|
89
|
+
echo " Se escala para revisión humana (el corte a modo runtime queda bloqueado mientras haya discrepancias sin resolver)." >&2
|
|
90
|
+
|
|
91
|
+
"$HERE/slice-ops.sh" escalate "Discrepancia dual: ${REASON_TEXT}" >/dev/null 2>&1 || true
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# event-emitter.sh — PostToolUse (Bash|Edit|Write|MultiEdit|Task) [EP-OR-08-B].
|
|
3
|
+
# La telemetría es propiedad del arnés, no del modelo: este hook NO puede omitirse desde
|
|
4
|
+
# el prompt. Encola en .claude/state/outbox/ y devuelve el control de inmediato — el
|
|
5
|
+
# despacho a POST /events lo hace el daemon (protocolo §2/§5). Nunca abre socket.
|
|
6
|
+
# Contenido: SOLO metadatos (herramienta, ruta relativa, argv0). Jamás código fuente ni
|
|
7
|
+
# el comando completo (protocolo §8: el arnés no envía fuente al runtime, y un comando
|
|
8
|
+
# puede llevar credenciales).
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
source "$HERE/lib/runtime-client.sh"
|
|
12
|
+
source "$HERE/lib/projection.sh"
|
|
13
|
+
|
|
14
|
+
MODE="$(runtime_mode)"
|
|
15
|
+
[ "$MODE" = "legacy" ] && exit 0
|
|
16
|
+
|
|
17
|
+
payload="$(cat 2>/dev/null || true)"
|
|
18
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
20
|
+
SLICE_ID="$(projection_get active_slice.slice_id "")"
|
|
21
|
+
if [ -n "$SLICE_ID" ]; then
|
|
22
|
+
TMPP="$(mktemp)" || exit 0
|
|
23
|
+
printf '%s' "$payload" > "$TMPP"
|
|
24
|
+
EV="$(PROJECT_ROOT="$(config_root)" python3 - "$TMPP" <<'PY' 2>/dev/null
|
|
25
|
+
import json,os,re,shlex,sys
|
|
26
|
+
try:
|
|
27
|
+
p=json.load(open(sys.argv[1]))
|
|
28
|
+
except Exception:
|
|
29
|
+
raise SystemExit(0)
|
|
30
|
+
if not isinstance(p,dict):
|
|
31
|
+
raise SystemExit(0)
|
|
32
|
+
tool=p.get("tool_name") or ""
|
|
33
|
+
if not tool:
|
|
34
|
+
raise SystemExit(0)
|
|
35
|
+
ti=p.get("tool_input")
|
|
36
|
+
if not isinstance(ti,dict):
|
|
37
|
+
ti={}
|
|
38
|
+
out={"tool":tool}
|
|
39
|
+
if tool=="Bash":
|
|
40
|
+
cmd=str(ti.get("command") or "").strip()
|
|
41
|
+
# shlex.split (posix=True) respeta comillas: una asignación de entorno inline con
|
|
42
|
+
# valor citado y espacios (p.ej. `KEY="a b" cmd`) se tokeniza como UN solo token, no
|
|
43
|
+
# se trocea. Si el comando trae comillas sin cerrar u otro problema de tokenización,
|
|
44
|
+
# shlex lanza ValueError: no arriesgamos un fragmento de secreto, degradamos a "".
|
|
45
|
+
try:
|
|
46
|
+
toks=shlex.split(cmd) if cmd else []
|
|
47
|
+
except ValueError:
|
|
48
|
+
toks=[]
|
|
49
|
+
# Salta asignaciones de variable de entorno inline al frente (p.ej. `VAR=secreto cmd`,
|
|
50
|
+
# `A=1 B=2 cmd`) para que argv0 sea siempre el binario, nunca un valor que pueda
|
|
51
|
+
# llevar credenciales (protocolo §8).
|
|
52
|
+
i=0
|
|
53
|
+
while i < len(toks) and re.match(r'^[A-Za-z_][A-Za-z0-9_]*=', toks[i]):
|
|
54
|
+
i += 1
|
|
55
|
+
out["argv0"]=toks[i] if i < len(toks) else ""
|
|
56
|
+
elif tool=="Task":
|
|
57
|
+
out["subagent_type"]=str(ti.get("subagent_type") or "")
|
|
58
|
+
else:
|
|
59
|
+
fp=str(ti.get("file_path") or ti.get("path") or "")
|
|
60
|
+
root=os.environ.get("PROJECT_ROOT") or ""
|
|
61
|
+
if root and fp.startswith(root+os.sep):
|
|
62
|
+
fp=fp[len(root)+1:]
|
|
63
|
+
out["path"]=fp
|
|
64
|
+
print(json.dumps(out,ensure_ascii=False))
|
|
65
|
+
PY
|
|
66
|
+
)"
|
|
67
|
+
rm -f "$TMPP"
|
|
68
|
+
[ -n "$EV" ] && runtime_enqueue_event "tool_use_recorded" "$EV" "$SLICE_ID"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Comprobación barata del daemon (stat del pidfile + kill -0, SIN red): un daemon caído a
|
|
72
|
+
# mitad de sesión no debe quedar muerto durante horas hasta el próximo SessionStart.
|
|
73
|
+
bash "$HERE/heartbeat.sh" --ensure "$PPID" >/dev/null 2>&1
|
|
74
|
+
|
|
75
|
+
exit 0
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# gitflow-guard.sh — PreToolUse · Bash
|
|
3
3
|
# Enforce GitHub Flow estricto:
|
|
4
|
-
# - Prohíbe `git commit` directo en
|
|
5
|
-
# - Prohíbe `git push` que apunte a
|
|
6
|
-
# - Exige trabajar en rama
|
|
7
|
-
#
|
|
8
|
-
|
|
4
|
+
# - Prohíbe `git commit` directo en las ramas protegidas.
|
|
5
|
+
# - Prohíbe `git push` que apunte a una rama protegida (la integración va por PR).
|
|
6
|
+
# - Exige trabajar en una rama con prefijo declarado para commitear.
|
|
7
|
+
# [EP-OR-08-B] Correcciones:
|
|
8
|
+
# 1. La rama se resuelve en el repo del COMANDO (`git -C <dir>`, `cd <dir> && git …`),
|
|
9
|
+
# no en el de la sesión: en sesiones multi-repo el guard evaluaba el repo equivocado
|
|
10
|
+
# (spec §9). Sin pistas en el comando, se cae al repo de la sesión, como antes.
|
|
11
|
+
# El comando se tokeniza en segmentos (por &&, ||, ; y |) y se aplica el efecto
|
|
12
|
+
# ACUMULATIVO de los `cd`: si hay varios `cd` antes del segmento que ejecuta el git,
|
|
13
|
+
# gana el ÚLTIMO (el directorio real en el que corre el commit), no el primero.
|
|
14
|
+
# 2. La política (prefijos y ramas protegidas) sale del asset sincronizado
|
|
15
|
+
# .claude/config/policy.json — el mismo que escribe context-sync.sh. Sin fichero,
|
|
16
|
+
# los defaults son los de v0.8.5: feature|fix|chore y main|master.
|
|
17
|
+
# 3. Los prefijos/ramas de la política se escapan (re.escape) antes de entrar al patrón
|
|
18
|
+
# grep -E: son texto de un asset editable, no regex de confianza, y un metacaracter
|
|
19
|
+
# (p.ej. un punto) no debe interpretarse como comodín.
|
|
20
|
+
# 4. `-C <dir>` solo cuenta como cambio de repo cuando es el flag GLOBAL de git (aparece
|
|
21
|
+
# antes del primer subcomando, p.ej. `git -C <dir> commit`). `git commit -C <ref>`
|
|
22
|
+
# es un flag del subcomando commit (reusa mensaje/autoría de otro commit) y NO cambia
|
|
23
|
+
# de directorio: tratarlo como tal dejaba el commit sin veredicto (bypass).
|
|
24
|
+
# Cero red (PreToolUse). `set -uo pipefail` — nunca `-e`: este hook no puede abortar a
|
|
25
|
+
# mitad y dejar pasar un commit sin veredicto.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
source "$HERE/lib/config.sh"
|
|
9
29
|
|
|
10
30
|
INPUT="$(cat)"
|
|
11
31
|
|
|
@@ -39,27 +59,157 @@ if [ "$is_commit" = false ] && [ "$is_push" = false ]; then
|
|
|
39
59
|
exit 0
|
|
40
60
|
fi
|
|
41
61
|
|
|
42
|
-
|
|
62
|
+
# ── Repo del comando ──
|
|
63
|
+
# Tokeniza el comando en segmentos (por &&, ||, ; y |) y camina en orden: cada `cd`
|
|
64
|
+
# actualiza el directorio "vigente" (acumulativo — gana el ÚLTIMO visto); al llegar al
|
|
65
|
+
# segmento que invoca el git de escritura, ese directorio vigente es el candidato, salvo
|
|
66
|
+
# que el propio segmento traiga `-C <dir>` como flag GLOBAL de git (antes del subcomando),
|
|
67
|
+
# que gana por ser más explícito y local a esa invocación.
|
|
68
|
+
REPO_DIR="$(CMD="$CMD" python3 - <<'PY' 2>/dev/null
|
|
69
|
+
import os,re,shlex
|
|
70
|
+
|
|
71
|
+
cmd = os.environ.get("CMD", "")
|
|
72
|
+
# Heurística de partición de shell (no un parser completo): &&, ||, ; y | en ese orden
|
|
73
|
+
# de prioridad para que "||" no se rompa en dos "|".
|
|
74
|
+
segments = re.split(r'&&|\|\||;|\|', cmd)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cd_target(seg):
|
|
78
|
+
m = re.match(r'^\s*cd\s+(.+?)\s*$', seg)
|
|
79
|
+
if not m:
|
|
80
|
+
return None
|
|
81
|
+
try:
|
|
82
|
+
toks = shlex.split(m.group(1).strip())
|
|
83
|
+
except Exception:
|
|
84
|
+
toks = [m.group(1).strip().strip("\"'")]
|
|
85
|
+
return toks[0] if toks else None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
ENV_ASSIGNMENT_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*=')
|
|
89
|
+
# Envoltorios comunes que anteponen el nombre del binario real sin ser el subcomando de
|
|
90
|
+
# git: `env FOO=x git ...`, `sudo git ...`. Solo se salta el nombre del envoltorio en sí
|
|
91
|
+
# (no sus propias flags) — cubre el caso reportado sin pretender ser un parser de sudo/env.
|
|
92
|
+
WRAPPER_CMDS = {"env", "sudo", "nice", "ionice", "time", "nohup"}
|
|
93
|
+
# Flags GLOBALES de git que toman un argumento en un token SEPARADO: si no se saltan como
|
|
94
|
+
# par, el walk trata el valor (p.ej. "user.name=x" de `-c user.name=x`) como si fuera el
|
|
95
|
+
# subcomando y para ahí, sin llegar nunca al `-C` real que viene después (bypass).
|
|
96
|
+
GIT_GLOBAL_FLAGS_WITH_ARG = {"-c", "-C", "--git-dir", "--work-tree", "--namespace", "--super-prefix"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def find_git_token(toks):
|
|
100
|
+
# Salta asignaciones de variables de entorno (`FOO=x git ...`) y envoltorios conocidos
|
|
101
|
+
# (`env`, `sudo`, ...) antes del `git` real — sin esto, `toks[0] != "git"` descarta el
|
|
102
|
+
# segmento entero y el -C real queda sin detectar (bypass: cae al repo de sesión).
|
|
103
|
+
i = 0
|
|
104
|
+
while i < len(toks) and (ENV_ASSIGNMENT_RE.match(toks[i]) or toks[i] in WRAPPER_CMDS):
|
|
105
|
+
i += 1
|
|
106
|
+
return i if i < len(toks) and toks[i] == "git" else None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def git_global_dash_c(seg):
|
|
110
|
+
# `-C <dir>` solo cuenta si aparece ANTES del primer subcomando (commit/push/…): ahí
|
|
111
|
+
# es el flag GLOBAL de git. Tras el subcomando es un flag suyo (p.ej. `commit -C <ref>`
|
|
112
|
+
# reusa mensaje/autoría de otro commit) y no debe leerse como cambio de directorio.
|
|
113
|
+
try:
|
|
114
|
+
toks = shlex.split(seg)
|
|
115
|
+
except Exception:
|
|
116
|
+
return None
|
|
117
|
+
gi = find_git_token(toks)
|
|
118
|
+
if gi is None:
|
|
119
|
+
return None
|
|
120
|
+
i = gi + 1
|
|
121
|
+
while i < len(toks):
|
|
122
|
+
t = toks[i]
|
|
123
|
+
if t == "-C" and i + 1 < len(toks):
|
|
124
|
+
return toks[i + 1]
|
|
125
|
+
if t in GIT_GLOBAL_FLAGS_WITH_ARG and i + 1 < len(toks):
|
|
126
|
+
i += 2 # flag GLOBAL con argumento en token separado (p.ej. -c user.name=x):
|
|
127
|
+
continue # se salta el PAR completo, nunca se lee el valor como subcomando.
|
|
128
|
+
if t.startswith("-"):
|
|
129
|
+
i += 1
|
|
130
|
+
continue
|
|
131
|
+
break # primer token que no es flag => subcomando; -C posterior no cuenta.
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
cur_dir = ""
|
|
136
|
+
target = ""
|
|
137
|
+
for seg in segments:
|
|
138
|
+
seg = seg.strip()
|
|
139
|
+
if not seg:
|
|
140
|
+
continue
|
|
141
|
+
cdt = cd_target(seg)
|
|
142
|
+
if cdt is not None:
|
|
143
|
+
cur_dir = cdt
|
|
144
|
+
continue
|
|
145
|
+
if re.search(r'(^|\s)git(\s|$)', seg) and re.search(r'\b(commit|push)\b', seg):
|
|
146
|
+
dashc = git_global_dash_c(seg)
|
|
147
|
+
target = dashc if dashc else cur_dir
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
print(target)
|
|
151
|
+
PY
|
|
152
|
+
)"
|
|
153
|
+
if [ -n "$REPO_DIR" ]; then
|
|
154
|
+
case "$REPO_DIR" in
|
|
155
|
+
/*) ;;
|
|
156
|
+
*) REPO_DIR="$(config_root)/$REPO_DIR" ;;
|
|
157
|
+
esac
|
|
158
|
+
else
|
|
159
|
+
REPO_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
160
|
+
fi
|
|
161
|
+
[ -d "$REPO_DIR" ] || REPO_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
162
|
+
|
|
163
|
+
BRANCH="$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
164
|
+
|
|
165
|
+
# ── Política (asset sincronizado, defaults v0.8.5) ──
|
|
166
|
+
POLICY="$(config_root)/.claude/config/policy.json"
|
|
167
|
+
POLICY_OUT="$(POLICY="$POLICY" python3 - <<'PY' 2>/dev/null
|
|
168
|
+
import json,os,re
|
|
169
|
+
try:
|
|
170
|
+
d=json.load(open(os.environ["POLICY"]))
|
|
171
|
+
except Exception:
|
|
172
|
+
d={}
|
|
173
|
+
if not isinstance(d,dict):
|
|
174
|
+
d={}
|
|
175
|
+
pref=[p for p in (d.get("branch_prefixes") or []) if isinstance(p,str)] or ["feature","fix","chore"]
|
|
176
|
+
prot=[p for p in (d.get("protected_branches") or []) if isinstance(p,str)] or ["main","master"]
|
|
177
|
+
# [EP-OR-08-B] re.escape: los prefijos/ramas vienen de un asset editable (policy.json), no son
|
|
178
|
+
# regex de confianza. Sin escapar, un metacaracter (p.ej. un punto en "release.candidate")
|
|
179
|
+
# se interpreta como comodín y afloja o rompe el gate silenciosamente.
|
|
180
|
+
pref_re="|".join(re.escape(p) for p in pref)
|
|
181
|
+
prot_re="|".join(re.escape(p) for p in prot)
|
|
182
|
+
print("^(" + pref_re + ")/", "^(" + prot_re + ")$")
|
|
183
|
+
PY
|
|
184
|
+
)"
|
|
185
|
+
case "$POLICY_OUT" in
|
|
186
|
+
*' '*) ;;
|
|
187
|
+
*) POLICY_OUT='^(feature|fix|chore)/ ^(main|master)$' ;;
|
|
188
|
+
esac
|
|
189
|
+
PREFIX_RE="${POLICY_OUT%% *}"
|
|
190
|
+
PROT_RE="${POLICY_OUT##* }"
|
|
43
191
|
|
|
44
192
|
block() {
|
|
45
193
|
echo "⛔ gitflow-guard (GitHub Flow estricto): $1" >&2
|
|
46
|
-
echo " Política:
|
|
194
|
+
echo " Política: ramas protegidas ${PROT_RE} · trabajar en ${PREFIX_RE}* · integrar por PR." >&2
|
|
195
|
+
echo " Repo evaluado: $REPO_DIR (rama '$BRANCH')." >&2
|
|
47
196
|
exit 2
|
|
48
197
|
}
|
|
49
198
|
|
|
50
|
-
# 1) Commit directo a
|
|
51
|
-
if [ "$is_commit" = true ] && printf '%s' "$BRANCH" | grep -Eq
|
|
199
|
+
# 1) Commit directo a una rama protegida.
|
|
200
|
+
if [ "$is_commit" = true ] && printf '%s' "$BRANCH" | grep -Eq "$PROT_RE"; then
|
|
52
201
|
block "commit directo a '$BRANCH' no permitido. Crea una rama: git switch -c feature/<slug>"
|
|
53
202
|
fi
|
|
54
203
|
|
|
55
204
|
# 2) Commit desde una rama no tipada.
|
|
56
|
-
if [ "$is_commit" = true ] && [ -n "$BRANCH" ] && ! printf '%s' "$BRANCH" | grep -Eq
|
|
57
|
-
block "rama '$BRANCH' no sigue la convención. Usa
|
|
205
|
+
if [ "$is_commit" = true ] && [ -n "$BRANCH" ] && ! printf '%s' "$BRANCH" | grep -Eq "$PREFIX_RE"; then
|
|
206
|
+
block "rama '$BRANCH' no sigue la convención. Usa un prefijo declarado en la política para commitear."
|
|
58
207
|
fi
|
|
59
208
|
|
|
60
|
-
# 3) Push apuntando a
|
|
61
|
-
|
|
62
|
-
|
|
209
|
+
# 3) Push apuntando a una rama protegida.
|
|
210
|
+
PROT_ALT="$(printf '%s' "$PROT_RE" | sed -e 's/^\^(//' -e 's/)\$$//')"
|
|
211
|
+
if [ "$is_push" = true ] && printf '%s' "$CMD" | grep -Eq "push[^|;&]*[ :]($PROT_ALT)([ ]|$|:)"; then
|
|
212
|
+
block "push directo a una rama protegida no permitido. Abre un Pull Request desde tu rama de trabajo."
|
|
63
213
|
fi
|
|
64
214
|
|
|
65
215
|
exit 0
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# heartbeat.sh — daemon singleton por repo [EP-OR-08-B, spec §4.3].
|
|
3
|
+
# uso: heartbeat.sh [--ensure <ppid>] | --daemon | --stop (default: --ensure $PPID)
|
|
4
|
+
#
|
|
5
|
+
# Por qué un daemon y no un hook: no existe evento Timer, los hooks son efímeros y un
|
|
6
|
+
# PostToolUse throttled NO late durante una tool call larga (suites de >5 min contra un
|
|
7
|
+
# TTL de 300 s). Ciclo de vida explícito:
|
|
8
|
+
# - Singleton por repo: pidfile + fichero de sesiones interesadas en .claude/state/.
|
|
9
|
+
# - Cada SessionStart registra su ppid y RELANZA el daemon si el pidfile está muerto;
|
|
10
|
+
# cada PostToolUse hace la misma comprobación barata (stat + kill -0, SIN red).
|
|
11
|
+
# - Muere cuando no queda NINGÚN ppid registrado vivo (no cuando muere el primero):
|
|
12
|
+
# dos sesiones sobre el mismo repo comparten daemon.
|
|
13
|
+
# - Deberes por tick: consumir el sentinela de flush y despachar la cola; cada
|
|
14
|
+
# lease_ttl_s/3, renovar el lease. Sin lease NO se apaga (los eventos de ámbito
|
|
15
|
+
# proyecto, pre-claim, necesitan despachador).
|
|
16
|
+
# - Nunca se cuelga de Stop (dispara cada turno) ni de SessionEnd (no existe).
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
source "$HERE/lib/runtime-client.sh"
|
|
20
|
+
source "$HERE/lib/projection.sh"
|
|
21
|
+
|
|
22
|
+
HB_TICK_S="${TRYCORE_HEARTBEAT_TICK_S:-5}"
|
|
23
|
+
case "$HB_TICK_S" in ''|*[!0-9]*) HB_TICK_S=5 ;; esac
|
|
24
|
+
|
|
25
|
+
hb_pidfile() { echo "$(config_root)/.claude/state/heartbeat.pid"; }
|
|
26
|
+
hb_sessions() { echo "$(config_root)/.claude/state/heartbeat-sessions.json"; }
|
|
27
|
+
hb_statusfile() { echo "$(config_root)/.claude/state/heartbeat-status.json"; }
|
|
28
|
+
hb_flushfile() { echo "$(runtime_outbox_dir)/.flush-request"; }
|
|
29
|
+
hb_spawnlock() { echo "$(config_root)/.claude/state/.heartbeat.spawn.lock"; }
|
|
30
|
+
|
|
31
|
+
# hb_alive — rc 0 si hay un daemon vivo según el pidfile.
|
|
32
|
+
hb_alive() {
|
|
33
|
+
local f pid
|
|
34
|
+
f="$(hb_pidfile)"
|
|
35
|
+
[ -f "$f" ] || return 1
|
|
36
|
+
pid="$(cat "$f" 2>/dev/null)"
|
|
37
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
38
|
+
kill -0 "$pid" 2>/dev/null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# hb_register_session <ppid> — upsert idempotente del ppid en el registro de sesiones.
|
|
42
|
+
hb_register_session() {
|
|
43
|
+
local ppid="${1:-}" f
|
|
44
|
+
case "$ppid" in ''|*[!0-9]*) return 0 ;; esac
|
|
45
|
+
f="$(hb_sessions)"
|
|
46
|
+
mkdir -p "$(dirname "$f")"
|
|
47
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
48
|
+
python3 - "$f" "$ppid" <<'PY' 2>/dev/null
|
|
49
|
+
import json,sys,os,tempfile
|
|
50
|
+
path,ppid=sys.argv[1],int(sys.argv[2])
|
|
51
|
+
try:
|
|
52
|
+
d=json.load(open(path))
|
|
53
|
+
except Exception:
|
|
54
|
+
d={}
|
|
55
|
+
if not isinstance(d,dict):
|
|
56
|
+
d={}
|
|
57
|
+
ppids=[p for p in (d.get("ppids") or []) if isinstance(p,int)]
|
|
58
|
+
if ppid not in ppids:
|
|
59
|
+
ppids.append(ppid)
|
|
60
|
+
d["ppids"]=ppids
|
|
61
|
+
dirn=os.path.dirname(path) or "."
|
|
62
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".heartbeat-sessions.",suffix=".tmp")
|
|
63
|
+
try:
|
|
64
|
+
with os.fdopen(fd,"w") as out:
|
|
65
|
+
json.dump(d,out,indent=2)
|
|
66
|
+
os.replace(tmp,path)
|
|
67
|
+
except Exception:
|
|
68
|
+
try: os.unlink(tmp)
|
|
69
|
+
except OSError: pass
|
|
70
|
+
PY
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# hb_prune_sessions — deja en el registro solo los ppids vivos e imprime cuántos quedan.
|
|
75
|
+
# La lista se materializa en un fichero temporal y se itera con `while read` (bash 3.2:
|
|
76
|
+
# nada de mapfile) para poder aplicar `kill -0` a cada uno.
|
|
77
|
+
hb_prune_sessions() {
|
|
78
|
+
local f tmp pid n
|
|
79
|
+
f="$(hb_sessions)"
|
|
80
|
+
[ -f "$f" ] || { echo 0; return 0; }
|
|
81
|
+
command -v python3 >/dev/null 2>&1 || { echo 0; return 0; }
|
|
82
|
+
tmp="$(mktemp)" || { echo 0; return 0; }
|
|
83
|
+
python3 - "$f" > "$tmp" <<'PY' 2>/dev/null
|
|
84
|
+
import json,sys
|
|
85
|
+
try:
|
|
86
|
+
d=json.load(open(sys.argv[1]))
|
|
87
|
+
except Exception:
|
|
88
|
+
d={}
|
|
89
|
+
for p in (d.get("ppids") or []):
|
|
90
|
+
print(p)
|
|
91
|
+
PY
|
|
92
|
+
local live=()
|
|
93
|
+
while IFS= read -r pid; do
|
|
94
|
+
case "$pid" in ''|*[!0-9]*) continue ;; esac
|
|
95
|
+
kill -0 "$pid" 2>/dev/null && live+=("$pid")
|
|
96
|
+
done < "$tmp"
|
|
97
|
+
rm -f "$tmp"
|
|
98
|
+
n=${#live[@]}
|
|
99
|
+
if [ "$n" -gt 0 ]; then
|
|
100
|
+
python3 - "$f" "${live[@]}" <<'PY' 2>/dev/null
|
|
101
|
+
import json,sys,os,tempfile
|
|
102
|
+
path=sys.argv[1]
|
|
103
|
+
ppids=[int(x) for x in sys.argv[2:]]
|
|
104
|
+
dirn=os.path.dirname(path) or "."
|
|
105
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".heartbeat-sessions.",suffix=".tmp")
|
|
106
|
+
try:
|
|
107
|
+
with os.fdopen(fd,"w") as out:
|
|
108
|
+
json.dump({"ppids":ppids},out,indent=2)
|
|
109
|
+
os.replace(tmp,path)
|
|
110
|
+
except Exception:
|
|
111
|
+
try: os.unlink(tmp)
|
|
112
|
+
except OSError: pass
|
|
113
|
+
PY
|
|
114
|
+
fi
|
|
115
|
+
echo "$n"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# hb_write_status <json> — estado observable del daemon (lo lee statusline-bridge.sh).
|
|
119
|
+
hb_write_status() {
|
|
120
|
+
local body f
|
|
121
|
+
body="$1"
|
|
122
|
+
f="$(hb_statusfile)"
|
|
123
|
+
mkdir -p "$(dirname "$f")"
|
|
124
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
125
|
+
python3 - "$f" "$body" <<'PY' 2>/dev/null
|
|
126
|
+
import json,sys,os,tempfile
|
|
127
|
+
path,body=sys.argv[1],sys.argv[2]
|
|
128
|
+
try:
|
|
129
|
+
d=json.loads(body)
|
|
130
|
+
except Exception:
|
|
131
|
+
d={}
|
|
132
|
+
dirn=os.path.dirname(path) or "."
|
|
133
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".heartbeat-status.",suffix=".tmp")
|
|
134
|
+
try:
|
|
135
|
+
with os.fdopen(fd,"w") as out:
|
|
136
|
+
json.dump(d,out,indent=2)
|
|
137
|
+
os.replace(tmp,path)
|
|
138
|
+
except Exception:
|
|
139
|
+
try: os.unlink(tmp)
|
|
140
|
+
except OSError: pass
|
|
141
|
+
PY
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# hb_renew — renueva el lease del slice activo. El servidor responde SIEMPRE 409 de cuerpo
|
|
146
|
+
# único cuando el lease ya no es nuestro (anti-oráculo HU-OR-41a): esa es la señal de
|
|
147
|
+
# «lease perdido». No se apaga el daemon; se marca el estado y se sigue despachando.
|
|
148
|
+
hb_renew() {
|
|
149
|
+
local slice_id agent_key resp status
|
|
150
|
+
slice_id="$(projection_get active_slice.slice_id "")"
|
|
151
|
+
[ -n "$slice_id" ] || return 0
|
|
152
|
+
agent_key="$(runtime_field agent_key)"
|
|
153
|
+
resp="$(runtime_put "/leases/renew" "{\"agent_key\":\"$agent_key\",\"slice_id\":\"$slice_id\"}")"
|
|
154
|
+
status="$(runtime_http_status)"
|
|
155
|
+
case "$status" in
|
|
156
|
+
200)
|
|
157
|
+
hb_write_status "{\"lease_lost\": false, \"slice_id\": \"$slice_id\", \"last_renew_status\": 200}"
|
|
158
|
+
return 0
|
|
159
|
+
;;
|
|
160
|
+
409)
|
|
161
|
+
hb_write_status "{\"lease_lost\": true, \"slice_id\": \"$slice_id\", \"last_renew_status\": 409}"
|
|
162
|
+
return 1
|
|
163
|
+
;;
|
|
164
|
+
*)
|
|
165
|
+
# Red caída: no se toca el estado anterior (no es lo mismo «sin red» que «lease perdido»).
|
|
166
|
+
return 1
|
|
167
|
+
;;
|
|
168
|
+
esac
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
hb_daemon() {
|
|
172
|
+
local pidf interval ttl last now n
|
|
173
|
+
pidf="$(hb_pidfile)"
|
|
174
|
+
mkdir -p "$(dirname "$pidf")"
|
|
175
|
+
printf '%s' "$$" > "$pidf"
|
|
176
|
+
trap 'rm -f "$(hb_pidfile)"; exit 0' TERM INT EXIT
|
|
177
|
+
interval="${TRYCORE_HEARTBEAT_INTERVAL_S:-}"
|
|
178
|
+
case "$interval" in
|
|
179
|
+
''|*[!0-9]*)
|
|
180
|
+
ttl="$(runtime_field lease_ttl_s)"
|
|
181
|
+
case "$ttl" in ''|*[!0-9]*) ttl=300 ;; esac
|
|
182
|
+
interval=$(( ttl / 3 ))
|
|
183
|
+
[ "$interval" -lt 10 ] && interval=10
|
|
184
|
+
;;
|
|
185
|
+
esac
|
|
186
|
+
last=0
|
|
187
|
+
while :; do
|
|
188
|
+
sleep "$HB_TICK_S"
|
|
189
|
+
n="$(hb_prune_sessions)"
|
|
190
|
+
case "$n" in ''|*[!0-9]*) n=0 ;; esac
|
|
191
|
+
[ "$n" -eq 0 ] && break # murió la última sesión interesada: el daemon se va con ella
|
|
192
|
+
if [ -f "$(hb_flushfile)" ]; then
|
|
193
|
+
rm -f "$(hb_flushfile)"
|
|
194
|
+
runtime_dispatch_outbox >/dev/null 2>&1
|
|
195
|
+
fi
|
|
196
|
+
now="$(date +%s)"
|
|
197
|
+
if [ $(( now - last )) -ge "$interval" ]; then
|
|
198
|
+
hb_renew
|
|
199
|
+
runtime_dispatch_outbox >/dev/null 2>&1
|
|
200
|
+
last="$now"
|
|
201
|
+
fi
|
|
202
|
+
done
|
|
203
|
+
rm -f "$pidf"
|
|
204
|
+
exit 0
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# hb_spawn — lanza el daemon bajo un mutex de directorio (`mkdir` es atómico): dos
|
|
208
|
+
# SessionStart simultáneos sobre el mismo repo no crean dos daemons. Un lock huérfano
|
|
209
|
+
# (proceso muerto entre el mkdir y el rmdir) se libera por edad.
|
|
210
|
+
#
|
|
211
|
+
# El mutex cubre «daemon confirmado arriba», no solo «proceso bifurcado»: tras el nohup se
|
|
212
|
+
# hace poll acotado (~3s) de hb_alive antes de soltar el lock. Sin esto, la ventana entre
|
|
213
|
+
# el backgrounding y que el hijo escriba heartbeat.pid deja el lock libre con hb_alive aún
|
|
214
|
+
# en falso; un --ensure casi simultáneo puede colarse ahí y lanzar un SEGUNDO daemon (se
|
|
215
|
+
# autosana en el siguiente tick al ver 0 sesiones propias, pero produce renew/dispatch
|
|
216
|
+
# duplicados transitorios mientras tanto).
|
|
217
|
+
hb_spawn() {
|
|
218
|
+
local lock waited
|
|
219
|
+
lock="$(hb_spawnlock)"
|
|
220
|
+
mkdir -p "$(dirname "$lock")"
|
|
221
|
+
if [ -d "$lock" ] && [ -n "$(find "$lock" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
222
|
+
rmdir "$lock" 2>/dev/null
|
|
223
|
+
fi
|
|
224
|
+
mkdir "$lock" 2>/dev/null || return 0 # otro proceso está lanzando: nada que hacer
|
|
225
|
+
if ! hb_alive; then
|
|
226
|
+
nohup bash "$HERE/heartbeat.sh" --daemon >/dev/null 2>&1 &
|
|
227
|
+
waited=0
|
|
228
|
+
while [ "$waited" -lt 30 ]; do
|
|
229
|
+
hb_alive && break
|
|
230
|
+
sleep 0.1
|
|
231
|
+
waited=$((waited + 1))
|
|
232
|
+
done
|
|
233
|
+
fi
|
|
234
|
+
rmdir "$lock" 2>/dev/null
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
hb_ensure() {
|
|
239
|
+
[ "$(runtime_mode)" = "legacy" ] && return 0
|
|
240
|
+
hb_register_session "${1:-}"
|
|
241
|
+
hb_alive || hb_spawn
|
|
242
|
+
return 0
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
hb_stop() {
|
|
246
|
+
local pid
|
|
247
|
+
pid="$(cat "$(hb_pidfile)" 2>/dev/null)"
|
|
248
|
+
case "$pid" in ''|*[!0-9]*) ;; *) kill "$pid" 2>/dev/null ;; esac
|
|
249
|
+
rm -f "$(hb_pidfile)" "$(hb_sessions)"
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case "${1:-}" in
|
|
254
|
+
--daemon) hb_daemon ;;
|
|
255
|
+
--stop) hb_stop ;;
|
|
256
|
+
--ensure) hb_ensure "${2:-$PPID}" ;;
|
|
257
|
+
*) hb_ensure "$PPID" ;;
|
|
258
|
+
esac
|
|
259
|
+
exit 0
|