@trycore/spec-build-harness 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/INSTALL.md +56 -3
- package/METODOLOGIA.md +6 -1
- package/README.md +46 -2
- package/VERSION +1 -1
- package/agents/build/dor-dod-gatekeeper.md +10 -3
- package/commands/build/epic.md +109 -0
- package/commands/build/onboard.md +34 -9
- package/commands/build/slice.md +4 -2
- package/commands/build/work.md +6 -2
- package/config/build-config.template.json +2 -1
- package/dist/commands/init.js +13 -0
- package/dist/commands/status.js +13 -1
- package/dist/lib/paths.js +1 -0
- package/dist/lib/runtime-client.js +20 -0
- package/docs/commands.md +3 -2
- package/docs/getting-started.md +11 -1
- package/docs/hooks.md +11 -1
- package/docs/runtime/guia-modo-dual-y-migracion.md +89 -9
- package/docs/runtime/protocolo-cliente-runtime.md +64 -0
- package/hooks/build/design-source-guard.sh +12 -1
- package/hooks/build/heartbeat.sh +64 -4
- package/hooks/build/lib/agent-context.sh +16 -7
- package/hooks/build/lib/config.sh +25 -1
- package/hooks/build/lib/runtime-client.sh +540 -9
- package/hooks/build/lib/runtime-ops.sh +108 -0
- package/hooks/build/scaffold-guard.sh +14 -1
- package/hooks/build/slice-ops.sh +498 -16
- package/package.json +1 -1
- package/scripts/tests/test-config.sh +41 -0
- package/scripts/tests/test-hooks-runtime.sh +198 -3
- package/scripts/tests/test-install.sh +23 -0
- package/scripts/tests/test-runtime-client.sh +500 -0
- package/scripts/tests/test-skill-ops.sh +456 -0
- package/skills/building-a-slice/references/dor.md +21 -9
- package/skills/building-a-slice/references/foundation-contract.md +4 -0
- package/state/README.md +12 -0
|
@@ -108,6 +108,13 @@ ops_path_front_integration() { echo "/fronts/$1/members/$2/integration"; }
|
|
|
108
108
|
ops_path_mirror() { echo "/projects/$1/mirror/transitions"; }
|
|
109
109
|
ops_path_proposals() { echo "/projects/$1/context/agent-proposals"; }
|
|
110
110
|
|
|
111
|
+
# [#62] Propuesta de épica (hub#113/#114). El agente PROPONE; el hub decide la identidad
|
|
112
|
+
# (`EP-XXX`) al aprobar y un humano aprueba: por eso hay una ruta de creación y otra de
|
|
113
|
+
# consulta, y ninguna de publicación. Mismo patrón que las propuestas de asset
|
|
114
|
+
# (`ops_path_proposals`), que es su precedente en este cliente.
|
|
115
|
+
ops_path_epic_proposals() { echo "/projects/$1/epic-proposals"; }
|
|
116
|
+
ops_path_epic_proposal() { echo "/projects/$1/epic-proposals/$2"; }
|
|
117
|
+
|
|
111
118
|
ops_mode() { runtime_mode; }
|
|
112
119
|
|
|
113
120
|
# ops_slice_id — id del slice activo según la caché de proyección ("" si no hay).
|
|
@@ -289,3 +296,104 @@ PY
|
|
|
289
296
|
*) return 5 ;;
|
|
290
297
|
esac
|
|
291
298
|
}
|
|
299
|
+
|
|
300
|
+
# ── Ledger local de propuestas de épica [#62] ────────────────────────────────────────
|
|
301
|
+
# Memoria del ciclo «propongo → un humano aprueba → escribo el fichero con el código del
|
|
302
|
+
# hub». Vive fuera de build-state.json a propósito: en modo `runtime` ese fichero no es la
|
|
303
|
+
# fuente de verdad, y el ledger no es estado del pipeline sino el rastro de una conversación
|
|
304
|
+
# con el hub. Clave estable: `client_event_id` (lo asigna el encolado, existe desde el primer
|
|
305
|
+
# instante); `proposal_id` llega después, con el ack, y puede no llegar nunca (offline).
|
|
306
|
+
ops_epic_ledger_path() { echo "$(config_root)/.claude/state/epic-proposals.json"; }
|
|
307
|
+
|
|
308
|
+
# ops_epic_ledger_read — imprime {"proposals":[…]}; nunca lanza.
|
|
309
|
+
ops_epic_ledger_read() {
|
|
310
|
+
local f; f="$(ops_epic_ledger_path)"
|
|
311
|
+
command -v python3 >/dev/null 2>&1 || { echo '{"proposals":[]}'; return; }
|
|
312
|
+
python3 - "$f" <<'PY' 2>/dev/null || echo '{"proposals":[]}'
|
|
313
|
+
import json,sys
|
|
314
|
+
try:
|
|
315
|
+
d=json.load(open(sys.argv[1]))
|
|
316
|
+
ps=d.get("proposals")
|
|
317
|
+
print(json.dumps({"proposals": ps if isinstance(ps,list) else []},ensure_ascii=False))
|
|
318
|
+
except Exception:
|
|
319
|
+
print('{"proposals":[]}')
|
|
320
|
+
PY
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# ops_epic_ledger_upsert <entrada-json> — upsert por client_event_id: las claves que la
|
|
324
|
+
# entrada NO menciona se conservan (así un `epic-status` puede fijar el estado sin borrar el
|
|
325
|
+
# borrador que guardó `propose-epic`). Escritura atómica. rc 0/1.
|
|
326
|
+
ops_epic_ledger_upsert() {
|
|
327
|
+
local entry="$1" f
|
|
328
|
+
f="$(ops_epic_ledger_path)"
|
|
329
|
+
mkdir -p "$(dirname "$f")"
|
|
330
|
+
command -v python3 >/dev/null 2>&1 || return 1
|
|
331
|
+
python3 - "$f" "$entry" <<'PY' 2>/dev/null || return 1
|
|
332
|
+
import json,sys,os,tempfile
|
|
333
|
+
path,entry=sys.argv[1],sys.argv[2]
|
|
334
|
+
try:
|
|
335
|
+
d=json.load(open(path))
|
|
336
|
+
except Exception:
|
|
337
|
+
d={}
|
|
338
|
+
ps=d.get("proposals")
|
|
339
|
+
if not isinstance(ps,list):
|
|
340
|
+
ps=[]
|
|
341
|
+
e=json.loads(entry)
|
|
342
|
+
cid=e.get("client_event_id")
|
|
343
|
+
for i,p in enumerate(ps):
|
|
344
|
+
if isinstance(p,dict) and p.get("client_event_id")==cid:
|
|
345
|
+
p.update(e)
|
|
346
|
+
ps[i]=p
|
|
347
|
+
break
|
|
348
|
+
else:
|
|
349
|
+
ps.append(e)
|
|
350
|
+
dirn=os.path.dirname(path) or "."
|
|
351
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".epic-proposals.",suffix=".tmp")
|
|
352
|
+
try:
|
|
353
|
+
with os.fdopen(fd,"w") as out:
|
|
354
|
+
json.dump({"proposals":ps},out,indent=2,ensure_ascii=False); out.flush(); os.fsync(out.fileno())
|
|
355
|
+
os.replace(tmp,path)
|
|
356
|
+
except Exception:
|
|
357
|
+
try: os.unlink(tmp)
|
|
358
|
+
except OSError: pass
|
|
359
|
+
raise
|
|
360
|
+
PY
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# ops_epic_ack_read <client_event_id> — el ack que dejó el carril directo, o "".
|
|
364
|
+
ops_epic_ack_read() {
|
|
365
|
+
local f; f="$(runtime_outbox_dir)/acks/$1.json"
|
|
366
|
+
[ -f "$f" ] || { echo ""; return; }
|
|
367
|
+
command -v python3 >/dev/null 2>&1 || { echo ""; return; }
|
|
368
|
+
python3 - "$f" <<'PY' 2>/dev/null || echo ""
|
|
369
|
+
import json,sys
|
|
370
|
+
try:
|
|
371
|
+
print(json.dumps(json.load(open(sys.argv[1])),ensure_ascii=False))
|
|
372
|
+
except Exception:
|
|
373
|
+
print("")
|
|
374
|
+
PY
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# ops_epic_rejected_reason <client_event_id> — razón con la que el carril directo apartó la
|
|
378
|
+
# petición (o "" si no está apartada). Es lo que convierte «no pasa nada» en un diagnóstico.
|
|
379
|
+
ops_epic_rejected_reason() {
|
|
380
|
+
local dir; dir="$(runtime_outbox_dir)/rejected"
|
|
381
|
+
[ -d "$dir" ] || { echo ""; return; }
|
|
382
|
+
command -v python3 >/dev/null 2>&1 || { echo ""; return; }
|
|
383
|
+
python3 - "$dir" "$1" <<'PY' 2>/dev/null || echo ""
|
|
384
|
+
import json,os,sys
|
|
385
|
+
d,cid=sys.argv[1],sys.argv[2]
|
|
386
|
+
for name in sorted(os.listdir(d)):
|
|
387
|
+
if not name.endswith(".json"):
|
|
388
|
+
continue
|
|
389
|
+
try:
|
|
390
|
+
e=json.load(open(os.path.join(d,name)))
|
|
391
|
+
except Exception:
|
|
392
|
+
continue
|
|
393
|
+
if e.get("client_event_id")==cid:
|
|
394
|
+
print(e.get("rejected_reason") or "apartada sin razón registrada")
|
|
395
|
+
break
|
|
396
|
+
else:
|
|
397
|
+
print("")
|
|
398
|
+
PY
|
|
399
|
+
}
|
|
@@ -18,7 +18,20 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
18
18
|
source "$HERE/lib/projection.sh"
|
|
19
19
|
if [ "$(runtime_mode)" = "runtime" ]; then
|
|
20
20
|
CACHE="$(runtime_projection_path)"
|
|
21
|
-
[ -f "$CACHE" ]
|
|
21
|
+
if [ ! -f "$CACHE" ]; then
|
|
22
|
+
# Sin proyección hay dos casos opuestos y hay que separarlos: si el proyecto
|
|
23
|
+
# no tiene credenciales es auto-arme (aún no usa el arnés) y se deja pasar;
|
|
24
|
+
# si las tiene, este agente está DESCONECTADO del hub y seguir escribiendo
|
|
25
|
+
# código es justo lo que produce trabajo duplicado y choques entre workers.
|
|
26
|
+
if runtime_disconnected; then
|
|
27
|
+
echo "⛔ sin conexión con el hub: hay credenciales del proyecto pero no hay proyección local." >&2
|
|
28
|
+
echo " Este agente no sabe qué trabajo tiene asignado, así que no puede escribir código." >&2
|
|
29
|
+
echo " Recupérala con \`slice-ops.sh status\` (refresca /agent/context) y reclama con \`/build:claim\`." >&2
|
|
30
|
+
echo " Si esto es un worktree: el estado del arnés vive en el clon principal y no viaja al worktree." >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
exit 0 # auto-arme: el proyecto aún no usa el arnés, nada que vigilar
|
|
34
|
+
fi
|
|
22
35
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
23
36
|
echo "⛔ scaffold-guard: python3 no disponible; no puedo verificar el gate de scaffold. Instala python3 (trycore-build doctor)." >&2
|
|
24
37
|
exit 2
|