refactor(ambitos): Migración completa dtic-DASUTEN (A04) y dtic-BKPs (A03) + herramientas ADN

Migración dtic-DASUTEN a estándar A04:
- A04_dtic-DASUTEN.md: Manifiesto creado
- A04.P001 a A04.P005: Planes renombrados de P2601.*
- _hist/_hist_P2601_dasuten.md: Legacy archivado

Migración dtic-BKPs a estándar A03:
- A03_dtic-BKPs.md: Manifiesto creado
- A03.P001, A03.P002: Planes migrados
- docs/ambito/dtic-BKPs/: Nueva estructura

Herramientas ADN nuevas:
- adn/tools/bkps/: BKPs migrado a tools ADN
- adn/tools/cli/dron.rb: Vigía de tareas largas
- adn/tools/cli/copiloto.rb: Asistente CLI
- adn/tools/cli/novato.rb: CLI de aprendizaje

Limpieza:
- dtic-BKPs/ → _hist/dtic-BKPs/ (legacy archivado)
- adn/tools/cli/backup.rb → _hist/ (migrado a bkps.rb)

Otros:
- nodos/pcv-dasu1.md, srvv-data.md, srvv-nginx-rm.md: Actualizados
- adn/tools/core/conciliador.rb: Mejoras
- adn/tools/w-zombi/data/cmd.json: Actualizado

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-04-07 10:23:53 -03:00
co-authored by Claude Opus 4.6
parent 018e45e046
commit 1802c25194
61 changed files with 3148 additions and 1201 deletions
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ==========================================================
# adn/tools/cli/novato.rb — Vigía de tareas largas
# ----------------------------------------------------------
# El "novato" se encarga de tareas de bajo nivel:
# - Lanzar un comando y vigilar hasta que termine
# - Cerrar automáticamente el evento de bitácora al finalizar
# - Registrar resultado (éxito/fallo) en la descripción
#
# Principio: Menos es Más — una tool reutilizable para
# cualquier ámbito que necesite delegar tareas largas.
#
# Uso:
# ./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
# ./adn/tools/run novato vigilar --evento 1379 --pid 12345
# ./adn/tools/run novato estado
# ==========================================================
require 'optparse'
require 'fileutils'
require 'time'
require 'json'
module ADN
class SubcomandoNovato
NOVATO_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'novato')
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
def initialize(args, logger = nil)
@args = args
@logger = logger
FileUtils.mkdir_p(NOVATO_DIR)
end
def ejecutar
if @args.empty? || @args.first == 'help' || @args.first == '--help'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'lanzar'
cmd_lanzar(@args)
when 'vigilar'
cmd_vigilar(@args)
when 'estado'
cmd_estado
when 'limpiar'
cmd_limpiar
else
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
# ─── lanzar: ejecutar comando en background y vigilar ────────
def cmd_lanzar(args)
evento_id = nil
nota = nil
# Separar opciones del novato del comando a ejecutar
separador = args.index('--')
unless separador
puts "#{Color::RED}✗ Falta separador '--' antes del comando a ejecutar#{Color::RESET}"
puts "Uso: ./adn/tools/run novato lanzar --evento 1379 -- <comando>"
return
end
novato_args = args[0...separador]
comando_args = args[(separador + 1)..-1]
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento de bitácora a cerrar") { |e| evento_id = e }
opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n }
end.parse!(novato_args)
if comando_args.empty?
puts "#{Color::RED}✗ Falta el comando a ejecutar después de '--'#{Color::RESET}"
return
end
comando_str = comando_args.join(' ')
tarea_id = "novato_#{Time.now.strftime('%H%M%S')}_#{$$}"
log_file = File.join(NOVATO_DIR, "#{tarea_id}.log")
meta_file = File.join(NOVATO_DIR, "#{tarea_id}.json")
# Guardar metadata
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
comando: comando_str,
nota: nota,
inicio: Time.now.iso8601,
pid: nil,
estado: 'lanzando'
}
File.write(meta_file, JSON.pretty_generate(meta))
# Lanzar el proceso vigía en background
vigia_script = <<~BASH
#!/bin/bash
# Novato vigía — auto-generado
LOGFILE="#{log_file}"
METAFILE="#{meta_file}"
EVENTO_ID="#{evento_id}"
ADN_RUN="#{ADN_RUN}"
echo "[$(date)] Novato: Iniciando comando..." >> "$LOGFILE"
#{comando_str} >> "$LOGFILE" 2>&1
EXIT_CODE=$?
echo "[$(date)] Novato: Comando terminó con código $EXIT_CODE" >> "$LOGFILE"
# Actualizar metadata
ruby -rjson -e '
f = "'"$METAFILE"'"
m = JSON.parse(File.read(f))
m["fin"] = Time.now.iso8601
m["exit_code"] = #{'"$EXIT_CODE".to_i'}
m["estado"] = #{'"$EXIT_CODE".to_i'} == 0 ? "completado" : "fallido"
File.write(f, JSON.pretty_generate(m))
'
# Cerrar evento en bitácora si hay evento_id
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
HORA_FIN=$(date +%H:%M)
DURACION=$(( ($(date +%s) - #{Time.now.to_i}) / 60 ))
if [ "$EXIT_CODE" -eq 0 ]; then
RESULTADO="- 🤖 Novato: ✔ Completado exitosamente (${DURACION}min)"
else
RESULTADO="- 🤖 Novato: ✖ Falló con código $EXIT_CODE (${DURACION}min)"
fi
NOTA_BASE="#{(nota || '').gsub('"', '\\"')}"
if [ -n "$NOTA_BASE" ]; then
DESC_FINAL="${NOTA_BASE}
${RESULTADO}"
else
DESC_FINAL="#{comando_str}
${RESULTADO}"
fi
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN" --descripcion "$DESC_FINAL" >> "$LOGFILE" 2>&1
echo "[$(date)] Novato: Evento $EVENTO_ID cerrado a las $HORA_FIN ($RESULTADO)" >> "$LOGFILE"
fi
BASH
vigia_path = File.join(NOVATO_DIR, "#{tarea_id}.sh")
File.write(vigia_path, vigia_script)
FileUtils.chmod(0755, vigia_path)
pid = spawn("nohup bash #{vigia_path} &", [:out, :err] => '/dev/null')
Process.detach(pid)
# Actualizar meta con PID
meta[:pid] = pid
meta[:estado] = 'vigilando'
File.write(meta_file, JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Novato lanzado#{Color::RESET}"
puts " Tarea: #{tarea_id}"
puts " PID: #{pid}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
puts " Comando: #{comando_str}"
puts ""
puts "#{Color::DIM}El novato cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
puts "#{Color::DIM}Consultar: ./adn/tools/run novato estado#{Color::RESET}"
end
# ─── vigilar: adjuntarse a un PID existente ──────────────────
def cmd_vigilar(args)
evento_id = nil
pid = nil
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento") { |e| evento_id = e }
opts.on("--pid PID", Integer, "PID del proceso a vigilar") { |p| pid = p }
end.parse!(args)
unless pid
puts "#{Color::RED}✗ Falta --pid#{Color::RESET}"
return
end
tarea_id = "vigil_#{pid}_#{Time.now.strftime('%H%M%S')}"
log_file = File.join(NOVATO_DIR, "#{tarea_id}.log")
# Lanzar vigía para PID existente
vigia_cmd = <<~CMD
nohup bash -c '
tail --pid=#{pid} -f /dev/null 2>/dev/null
HORA_FIN=$(date +%%H:%%M)
echo "[$(date)] Novato: PID #{pid} terminó" >> #{log_file}
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
echo "[$(date)] Novato: Evento #{evento_id} cerrado a las $HORA_FIN" >> #{log_file}
' > /dev/null 2>&1 &
CMD
system(vigia_cmd)
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
pid_vigilado: pid,
inicio: Time.now.iso8601,
estado: 'vigilando'
}
File.write(File.join(NOVATO_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Novato vigilando PID #{pid}#{Color::RESET}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
end
# ─── estado: mostrar tareas del novato ───────────────────────
def cmd_estado
metas = Dir.glob(File.join(NOVATO_DIR, '*.json')).sort
if metas.empty?
puts "#{Color::DIM}Sin tareas de novato activas.#{Color::RESET}"
return
end
puts "#{Color::BOLD}📋 Tareas del Novato#{Color::RESET}\n\n"
metas.each do |f|
m = JSON.parse(File.read(f))
icono = case m['estado']
when 'completado' then "#{Color::GREEN}#{Color::RESET}"
when 'fallido' then "#{Color::RED}#{Color::RESET}"
when 'vigilando' then "#{Color::YELLOW}#{Color::RESET}"
else "#{Color::DIM}?#{Color::RESET}"
end
pid_info = m['pid'] || m['pid_vigilado']
vivo = pid_info && system("kill -0 #{pid_info} 2>/dev/null")
puts " #{icono} #{m['tarea_id']}"
puts " Evento: #{m['evento_id'] || '-'} PID: #{pid_info || '-'}#{vivo ? " #{Color::GREEN}(vivo)#{Color::RESET}" : ''}"
puts " #{m['comando'] || "Vigilando PID #{m['pid_vigilado']}"}"
puts " Inicio: #{m['inicio']}#{m['fin'] ? " Fin: #{m['fin']}" : ''}"
puts ""
end
end
# ─── limpiar: borrar tareas completadas ──────────────────────
def cmd_limpiar
Dir.glob(File.join(NOVATO_DIR, '*.json')).each do |f|
m = JSON.parse(File.read(f))
if ['completado', 'fallido'].include?(m['estado'])
base = File.basename(f, '.json')
['.json', '.log', '.sh'].each do |ext|
FileUtils.rm_f(File.join(NOVATO_DIR, "#{base}#{ext}"))
end
end
end
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
end
def mostrar_ayuda
puts <<~HELP
#{Color::BOLD}#{Color::CYAN}Novato — Vigía de Tareas Largas#{Color::RESET}
El novato se encarga de las tareas de bajo nivel: ejecutar comandos
largos en background y cerrar automáticamente eventos de bitácora
cuando terminan. Delegar sin desperdiciar recursos.
#{Color::YELLOW}Uso:#{Color::RESET}
novato lanzar --evento ID -- <comando> Lanzar y vigilar
novato vigilar --evento ID --pid PID Vigilar PID existente
novato estado Ver tareas activas
novato limpiar Limpiar completadas
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
./adn/tools/run novato vigilar --evento 1379 --pid 12345
./adn/tools/run novato estado
HELP
end
end
end