Skip to content

dagu-feature: triage de PoC y workflow poc.yaml #135

Closed

Description

@pablontiv

Problema

skills/dagu-feature entrega un issue de punta a punta, pero no sabe nada de la PoC de factibilidad. Las PoC se ejecutan fuera de Dagu y su flujo se rompe de forma recurrente. Evidencia de 110 ítems de a4s, homeserver, rootline y extensiones:

Cuando la implementación dependía de un comportamiento externo no ejercitado, hubo corrección tardía en 40 de 80 ítems; sin ese factor, en 5 de 30.

Hoy cada repo decide a mano si hace PoC, y nada impide implementar sin ella.

Alcance

triage en feature.yaml

  • Un nuevo primer paso triage (human.task) que completa la sesión que encoló el run, con estos inputs:
    • POC=yes|no;
    • QUESTION: el comportamiento a probar;
    • BASIS: con POC=no, la URL de un PASS previo o de un PR mergeado que ya ejercitó el comportamiento, o el motivo;
    • REVIEW: obligatorio con POC=no; es la referencia a la salida de un subagente reviewer.
  • El prompt de triage contiene el criterio: hay PoC cuando la implementación depende de un comportamiento externo o de runtime no ejercitado que puede cambiar la ruta de implementación. Reutilizar algo ya demostrado avanza directo. La reproducción de un bug no es una PoC.
  • Los pasos siguientes leen los inputs con ${steps.triage.outputs.<NAME>}. ${triage.output.<NAME>} queda como texto literal en Dagu 2.19.2.
  • La decisión se publica en el issue como un comentario cuya primera línea es un marcador <!-- poc-triage:<yes|no> run:<RUN> -->.
  • POC=yes: el run encola poc ISSUE=<N> QUESTION=<...> y termina.
  • POC=no sin REVIEW: triage falla.
  • Red de dependencias: checkpoint_push compara BASE_SHA..HEAD contra todas las dependencias del árbol base. Si encuentra una dependencia o un paquete nuevo, hace push_back a triage en lugar de fallar.
    • Fuentes: package.json, go.mod, pyproject/requirements, Chart/Flux, imágenes de Compose y directorios nuevos de paquete o app.

poc.yaml nuevo

  • Parámetros:
    • QUESTION: obligatorio si falta ISSUE;
    • ISSUE: opcional;
    • PRIMARY, BASE_BRANCH y la cola se renderizan con el bootstrap.
  • queue: __REPO__, la misma que usa feature.
  • RUN sale de ${context.run.id} en env.

Pasos:

  1. record: si falta ISSUE, crea el issue con la pregunta. Guarda el número en un archivo de estado por RUN bajo el git-dir de PRIMARY.

  2. base: exige PRIMARY limpio y en BASE_BRANCH, hace fetch y merge --ff-only, y emite BASE_SHA.

  3. worktree: git worktree add -b poc/${RUN} ${PRIMARY}/.workspace/worktrees/poc-${RUN} ${BASE_SHA}.

    • El archivo de resultado vive en git rev-parse --git-path poc/result.md.
    • La rama poc/* nunca se hace push.
  4. run_poc (human.task): el agente ejecuta los comandos mínimos y escribe el resultado con estas secciones:

    • VERDICT: PASS|FAIL|INCONCLUSIVE;
    • ## Pregunta;
    • ## Proposición demostrada;
    • ## Ruta ejercitada;
    • ## Comandos y salida;
    • ## Después del PASS: obligatoria con PASS.
      Un diff vacío es válido.
  5. scope_gate: falla si el diff BASE_SHA..worktree toca rutas de tests de la convención del repo o cualquier *.md.

    • Incluye los archivos sin seguimiento (git ls-files --others --exclude-standard -z), con nombres NUL-delimitados y ambos nombres en los renames.
    • También falla si el resultado no tiene un VERDICT válido o le faltan secciones.
  6. publish: publica un comentario cuya primera línea es <!-- poc-verdict:<V> scope:<OK|FAIL> run:<RUN> attempt:<N> base:<BASE_SHA> -->, seguido de:

    • el resultado;
    • el diffstat;
    • el parche completo, incluidos los archivos nuevos;
    • la referencia o digest de cada artefacto ejecutado (release, imagen).

    Si el comentario es demasiado grande, publica un preview y el parche codificado repartido en varios comentarios enlazados.

    Con PASS, copia ## Después del PASS al cuerpo del issue.

  7. decide (human.task):

    • push_back a run_poc para iterar con attempt+1, protegido con --expected-iteration;
    • el input promote=yes|no al completar.
  8. dispose:

    • PASS con promote=yes: exige ## Después del PASS en el cuerpo y encola feature con su contrato completo (ruta absoluta del DAG, ISSUE, PRIMARY, BRANCH y WORKTREE si el repo lo usa). Su triage cita el PASS como BASIS.
    • PASS con promote=no: termina.
    • FAIL en un issue creado por record: lo cierra como not planned, con el veredicto como razón.
    • INCONCLUSIVE: el operador decide en decide.
    • Issue preexistente: queda abierto con el veredicto.
  9. handler_on.failure: publica el intento leyendo los archivos por RUN. Si no hay resultado, registra un fallo de orquestación sin veredicto de capacidad.

  10. handler_on.exit: git worktree remove --force y borrado de la rama poc/${RUN}, solo si existen, todo derivado de RUN.

Bootstrap y skill

  • dagu-feature-bootstrap.yaml renderiza poc.yaml solo con PRIMARY, BASE_BRANCH, el repo y las rutas de tests. GATE, REQUIRED_CHECK y la protección de rama siguen siendo exclusivos de feature.yaml.
  • SKILL.md documenta triage, poc.yaml y cómo completar sus human.task.
  • skills/refine/SKILL.md, sección Spike: alinear la condición con el criterio de triage. Hay incertidumbre material cuando un comportamiento externo o de runtime no ejercitado determina la ruta de implementación.
  • Se instala en a4s con el bootstrap.

Fuera de alcance:

PoC

Los PoC ya se ejecutaron en sandbox y su veredicto está en los comentarios de este issue:

PoC Veredicto
Las 10 primitivas Dagu 2.19.2 + git del flujo PASS
Escritura y lectura en GitHub (record, marcadores, gate paginado, fences, pregunta editada, NOT_PLANNED, tamaño) PASS
La regla tests + *.md frente a los cutovers históricos PASS
Red de dependencias como bloqueo FAIL; por eso actúa como disparador de triage
Reviewer ciego en POC=no frente al histórico INCONCLUSIVE; se incluye y se mide con los marcadores

Aceptación

  • dagu validate pasa con feature.yaml y poc.yaml renderizados por el bootstrap en a4s.
  • En vivo en a4s:
    • un issue pasa por triage con POC=no y REVIEW y llega a merge;
    • triage con POC=no sin REVIEW falla;
    • triage con POC=yes encola poc.
  • En vivo, poc sin ISSUE:
    • crea el issue antes de run_poc;
    • itera una vez con push_back;
    • publica dos comentarios con marcador;
    • con FAIL, cierra el issue como not planned.
  • En vivo, poc con PASS y promote=yes: copia ## Después del PASS al cuerpo y encola feature, cuyo triage cita el PASS.
  • Un *.md en el diff de la PoC hace fallar scope_gate, y handler_on.failure publica el intento con scope:FAIL antes de limpiar.
  • Dos runs de poc esperan a la vez en run_poc con la cola de un cupo.
  • Una dependencia nueva en checkpoint_push devuelve el run a triage.
  • Al terminar no queda ningún worktree ni rama poc/*.

Activity

  1. pablontiv commented on Oct 14, 2026

    @pablontiv
    OwnerAuthor

    VERDICT: PASS

    Pregunta

    ¿Dagu 2.19.2 y git soportan de extremo a extremo las primitivas de poc.yaml más triage, con scheduler aislado, cola compartida de cupo uno, tareas humanas, iteración, handlers y promoción encolada, y puede poc compartir la serialización real de homeserver?

    Proposición demostrada

    Dagu 2.19.2 ejecuta el esqueleto completo triage → base → worktree → run_poc → scope_gate → publish → decide → dispose, incluyendo rewind humano→humano, inputs tipados leídos por pasos posteriores, publicación de fallo antes de limpieza, limpieza forzada idempotente y enqueue a otro DAG de la misma cola; git permite fast-forward de PRIMARY mientras otro worktree sigue activo. La exclusión de homeserver también puede compartirse, pero es un lock persistente fail-fast, no una cola que permita continuar a todos los runs humanos simultáneamente.

    Ruta ejercitada

    • Binario real: /Users/pones/.local/bin/dagu; dagu version → 2.19.2.
    • Todo dato y comando mutable dentro de /tmp/poc-PocDaguSkeleton-Ey4qwA/ (alias físico macOS /private/tmp/...). DAGU_HOME=$S/home, bare $S/origin.git, clone PRIMARY $S/primary, clone upstream $S/upstream, worktrees $S/worktrees/poc-$RUN, estado $S/state/$RUN.
    • Scheduler real env DAGU_HOME=$S/home dagu scheduler, puerto de scheduler 19848, puerto web configurado 19847; no servidor web necesario. Se detuvo únicamente el servicio sandbox antes de eliminar el directorio.
    • config.yaml: queues.enabled: true, queues.config: [{name: repo, max_concurrency: 1}]; ambos poc.yaml y feature.yaml tenían queue: repo.
    • poc usó type: graph, shell: bash, env.RUN: '${context.run.id}'; pasos ejecutaron scripts reales con git y archivos, sin gh.
    • triage usó action: human.task, with.form.type: object, propiedades string POC/QUESTION/BASIS, POC enum ['yes','no'], requeridas las tres.
    • Los inputs se mapearon explícitamente en base.env con ${steps.triage.outputs.POC}, ${steps.triage.outputs.QUESTION}, ${steps.triage.outputs.BASIS}. No son variables de entorno automáticas.
    • decide.with.push_back.rewind_to: run_poc; run_poc era también action: human.task.
    • handler_on.failure leyó exclusivamente archivos por RUN (base.sha, result) y escribió published-failure; handler_on.exit ejecutó limpieza sobre solo worktree/rama del RUN.
    • Homeserver se leyó sin modificar: .workspace/dagu/dags/homeserver-feature.yaml:1–102, scripts/lib.sh:16–65, scripts/sync.sh:1–33, scripts/cleanup.sh:1–35.
    • Las escrituras a origin fueron dos git push origin main solo al bare dentro del sandbox, sin red. Se notificó la interpretación de la regla al padre; Main aclaró explícitamente que este origin local está dentro del alcance y que la prohibición era para remotos reales. Ningún GitHub write ni repo externo modificado.

    Veredicto por primitiva

    # Primitiva Veredicto Evidencia
    1 triage con POC, QUESTION, BASIS legibles después PASS fourth/base recibió POC=yes, QUESTION=Can later steps read triage?, BASIS=Typed runtime outputs; second/triage aceptó POC=no.
    2 PRIMARY limpio + fetch + ff-only con otro worktree activo PASS second/base avanzó PRIMARY abc3616 → 510ff6b; poc-first siguió activo en abc3616.
    3 worktree desde BASE_SHA y RUN contextual PASS RUN=first/second/fourth; worktrees creados en los SHA registrados; ramas poc/first, poc/second, poc/fourth.
    4 run_poc human.task PASS first y second quedaron simultáneamente waiting en run_poc; completados por CLI.
    5 scope tracked + untracked PASS second/scope_gate: tracked.paths=b'source.txt\x00', untracked.paths=b'new file.txt\x00', mediante diff contra BASE_SHA y ls-files --others.
    6 decide push_back a run_poc humano + expected iteration PASS --expected-iteration 0 aceptado; run_poc reabrió en iteration 1; nuevo envío con expected 0 rechazado por ser iteration 1.
    7 failure publica desde archivos RUN PASS third/scope_gate falló intencionalmente; onFailure y onExit succeeded, archivo published-failure incluyó RUN, BASE_SHA y VERDICT: FAIL.
    8 exit elimina worktree dirty y rama; idempotente PASS Dirty files de first/second/fourth eliminados por worktree remove --force; branch -D; repetir el mismo script exit tras terminal devolvió 0. Al final solo PRIMARY/main.
    9 dispose encola segundo DAG en misma cola de cupo uno PASS first/second/fourth encolaron feature; archivo delivered: feature-first, feature-second, feature-fourth; feature-fourth succeeded a las 16:46:13, tras parent terminal 16:46:10.
    10 dos runs humanos liberan el cupo PASS Status simultáneo 16:43:24: first Waiting/run_poc y second Waiting/run_poc, ambos queue repo max_concurrency 1.
    11 compartir mecanismo homeserver PASS feature-lock adquirió owner lock; poc-lock concurrente rechazado preservando lock del owner; tras release, nuevo poc-lock acquired/released y succeeded.

    Intentos

    0. Descubrimiento y correcciones de interfaz

    1. command -v dagu || test -x /Users/Shared/harness/a4s/.workspace/dagu/bin/dagu && /Users/Shared/harness/a4s/.workspace/dagu/bin/dagu version imprimió /Users/pones/.local/bin/dagu pero por precedencia intentó además el fallback inexistente y terminó 127. Se detuvo esa llamada y se usó el PATH real.
    2. dagu version && dagu --help && dagu human --help confirmó 2.19.2; human no existe, la ayuda enumera human-task. Se corrigió el nombre, sin invocar tareas vivas.
    3. dagu schema human.task falló: primer argumento debe ser dag|config. dagu schema config server también falló pese al ejemplo en la ayuda. Se usaron dagu schema dag definitions.humanTaskActionConfig, ...humanTaskPushBackConfig, ...humanTaskFormSchema, y dagu schema config definitions.QueueDef.
    4. Primer arranque con solo port: 19847 anunció health server de scheduler en 8090. Se detuvo inmediatamente solo ese scheduler sandbox y se añadió scheduler: {port: 19848}. Segundo arranque confirmó service registry y health server en 19848. No se tocó el daemon vivo.

    1. Git real, dos checkpoints concurrentes y fast-forward

    Comandos (S es el sandbox anterior; cada dagu se ejecutó con DAGU_HOME=$S/home):

    git init --bare --initial-branch=main "$S/origin.git"
    git clone "$S/origin.git" "$S/primary"
    git -C "$S/primary" -c user.name=PoC -c user.email=poc@example.invalid commit --allow-empty -m initial
    git -C "$S/primary" push origin main
    git clone "$S/origin.git" "$S/upstream"
    dagu enqueue --run-id first poc
    dagu enqueue --run-id second poc
    dagu human-task complete poc --run-id first --step triage --input POC=yes --input 'QUESTION=Does the skeleton work?' --input 'BASIS=Real runtime PoC'

    Primera observación inmediata: first Waiting, second Queued. No se concluyó bloqueo; el siguiente despacho del scheduler puso second Waiting a las 16:42:37, mientras first permanecía waiting. El diagnóstico definitivo fue esperar el despacho real, no asumir simultaneidad instantánea.

    base ejecutó en este orden:

    [[ $(git -C "$PRIMARY" symbolic-ref --short HEAD) == main ]]
    [[ -z $(git -C "$PRIMARY" status --porcelain --untracked-files=all) ]]
    git -C "$PRIMARY" fetch origin
    git -C "$PRIMARY" merge --ff-only origin/main
    git -C "$PRIMARY" rev-parse HEAD > "$STATE/base.sha"
    git -C "$PRIMARY" worktree add -b "poc/$RUN" "$WT" "$(cat "$STATE/base.sha")"

    Con first ya esperando en run_poc, se escribió upstream/source.txt, git add source.txt, commit -m upstream y push al bare local; luego:

    dagu human-task complete poc --run-id second --step triage --input POC=no --input 'QUESTION=Known behavior' --input 'BASIS=Existing evidence'
    git -C "$S/primary" worktree list
    dagu status --run-id first poc
    dagu status --run-id second poc

    Salida: PRIMARY main 510ff6b, poc-first abc3616, poc-second 510ff6b. Ambos run_poc [waiting]. second/base mostró Updating abc3616..510ff6b / Fast-forward.

    2. Inputs: dos rutas no válidas y ruta válida

    • Primer intento: completar triage no exportó POC/QUESTION/BASIS automáticamente al entorno de base. El status persistido sí tenía stepOutputsValue con los tres valores.
    • Segundo intento, run third: base.env.POC: '${triage.output.POC}' y análogos quedaron literales en el entorno. La forma del ejemplo genérico dagu example 2 no fue la ruta válida para estos outputs declarados de human.task.
    • Se corrigió a ${steps.triage.outputs.POC} y equivalentes, se encoló un run nuevo fourth (los runs anteriores conservan su snapshot), y se completó:
    dagu human-task complete poc --run-id fourth --step triage --input POC=yes --input 'QUESTION=Can later steps read triage?' --input 'BASIS=Typed runtime outputs'

    fourth/base env real: POC=yes, QUESTION=Can later steps read triage?, BASIS=Typed runtime outputs, RUN=fourth. Este es el run corregido que llegó hasta dispose y child feature succeeded.

    3. scope_gate, rewind humano→humano y protección de iteración

    Se modificó poc-second/source.txt (tracked) y se creó poc-second/new file.txt (untracked); run_poc se completó por CLI. scope_gate ejecutó:

    git -C "$WT" diff --name-only -z "$(cat "$STATE/base.sha")" > "$STATE/tracked.paths"
    git -C "$WT" ls-files --others --exclude-standard -z > "$STATE/untracked.paths"

    Bytes observados: source.txt\0, new file.txt\0. El gate de esta prueba registra el conjunto de rutas; no implementa la política de clasificación de tests/docs (otro PoC).

    dagu human-task push-back poc --run-id second --step decide --expected-iteration 0
    # Pushed back human task decide to run_poc; DAG-run queued for resume.
    dagu human-task push-back poc --run-id fourth --step decide --expected-iteration 0
    # mismo éxito; status posterior: run_poc waiting, push-back iteration: 1
    dagu human-task complete poc --run-id fourth --step run_poc
    dagu human-task push-back poc --run-id fourth --step decide --expected-iteration 0
    # Error: human task step "decide" is at push-back iteration 1, not 0 (exit 1, rechazo esperado)
    dagu human-task complete poc --run-id fourth --step decide

    Rewind conservó worktree y BASE_SHA; no repitió base/worktree. Se repitieron scope_gate/publish tras completar el humano rebobinado.

    4. Fallo, publicación y limpieza idempotente

    En third/state se escribió result con VERDICT: FAIL y un flag force-failure; el script scope_gate termina con [[ ! -e "$STATE/force-failure" ]], fallo intencional de orquestación sin GitHub. Tras completar run_poc:

    • scope_gate failed, publish/decide/dispose aborted;
    • handler_on.failure succeeded y escribió published-failure:
      orchestration/scope failure run=third base=510ff6beac0e72cba273f73f8497b758da504af9 result=VERDICT: FAIL seguido de real stub result;
    • evento ordenado third scope_gate → third failure → third exit;
    • handler_on.exit succeeded, eliminó poc/third.

    La limpieza ejecutada por handler fue:

    if [[ -d "$WT" ]]; then git -C "$PRIMARY" worktree remove --force "$WT"; fi
    if git -C "$PRIMARY" show-ref --verify --quiet "refs/heads/$BRANCH"; then git -C "$PRIMARY" branch -D "$BRANCH"; fi

    Se habían creado archivos dirty untracked en first y fourth; second tenía además tracked dirty. Sus handlers eliminaron todos. Invocar otra vez el mismo script con ROOT/RUN first, third y fourth, tras terminar, devolvió 0. Al final git worktree list mostró solo PRIMARY 510ff6b [main], git branch --list solo main.

    5. dispose y child en la misma cola

    Paso dispose real:

    dagu enqueue --run-id "feature-$RUN" "$DAGU_HOME/dags/feature.yaml"

    No espera al child. feature usó queue repo y un run printf '%s\n' "$RUN" >> delivered.

    fourth final: todos los pasos succeeded, dispose encoló feature-fourth, onExit eliminó rama; parent terminal a las 16:46:10. dagu status --run-id feature-fourth feature → Succeeded a las 16:46:13. Archivo delivered: feature-first, feature-second, feature-fourth. No deadlock con max_concurrency 1.

    6. Serialización real homeserver replicada

    homeserver-feature.yaml no tiene queue ni lock declarado; max_active_steps: 1 solo controla pasos del run. El mecanismo compartido está en los scripts: sync.sh llama acquire_lock, lib.sh fija LOCK=$COMMON/homeserver-dagu.lock, OWNER es SPACE + newline + RUN, publica un archivo owner ya escrito mediante ln atómico exclusivo. Si existe un owner ajeno, falla inmediatamente. cleanup.sh libera el lock propio al final y rechaza liberar el ajeno. Se replicaron esas funciones en $S/lock.sh, con COMMON del git PRIMARY sandbox; ninguna función init del repo real se ejecutó.

    • feature-lock (queue repo) adquirió lock como local\nlock-owner y quedó waiting en human.task hold; el exit handler aún no había corrido.
    • poc-lock (misma queue repo) con lock-contender falló al adquirir: repository lock belongs to another run; recovery must name its owner. Su exit falló de forma protectora: cleanup cannot release another run lock. El feature-lock continuó Waiting. Este rechazo era el resultado esperado de compartir la exclusión, no un error desconocido.
    • Se completó hold de lock-owner; su exit liberó el lock.
    • dagu enqueue --run-id lock-poc-success poc-lock → Succeeded; base acquired y exit released.
    • lock-events exacto: acquired lock-owner, released lock-owner, acquired lock-poc-success, released lock-poc-success.

    Implicación para el diseño

    1. Las diez primitivas de Dagu/git de la pregunta pasan; no hace falta cambiar Dagu para el esqueleto propuesto. Los inputs humanos deben usarse por ${steps.triage.outputs.KEY} y mapearse a env si el script los necesita. Un script no recibe automáticamente POC/QUESTION/BASIS.
    2. env.RUN: '${context.run.id}' funciona en steps y handlers; archivos por RUN bastan para publicar sin outputs de pasos en handlers.
    3. Los humanos liberan el slot de la cola y mantienen sus worktrees hasta la terminación real. El scheduler tiene latencia de despacho: un status Queued observado inmediatamente no demuestra que el humano retenga el slot.
    4. decide → run_poc humano→humano funciona, mantiene BASE_SHA/worktree y protege la iteración con expected-iteration. La iteración inicial es 0, tras rewind 1.
    5. Encolar desde dispose a la misma cola funciona porque enqueue es asíncrono; el child corre cuando termina el parent. El registro de fallo se escribe antes de exit, y --force limpia worktrees dirty con idempotencia guardada por existencia.
    6. Homeserver requiere adaptar explícitamente su lock existente. Una queue nueva en poc no lo comparte por sí sola. Puede usarse el mismo owner lock y coexistir con seguridad, como se demostró; pero conservar su lifecycle actual retiene el lock durante el humano y hace fallar a otros runs aunque el slot Dagu esté libre. La promesa “varias PoC/features esperando y mutaciones git cortas serializadas” queda demostrada para la cola, no para ese lock de run completo. Si se exige esa promesa también en homeserver, hay que cambiar su alcance de lock o adoptar una cola común en ambos DAGs y demostrar ese cutover por separado.
    7. Este PASS cubre ejecución y stubs de publicación. No afirma haber probado GitHub, promoción real de homeserver, policy tests/docs, clasificación de dependencias, ni guardas omp de sesión (fuera de esta pregunta).
    8. Sandbox eliminado tras guardar esta evidencia; no queda scheduler ni código de prueba instalado. Se conserva únicamente este veredicto.
  2. pablontiv commented on Oct 14, 2026

    @pablontiv
    OwnerAuthor

    VERDICT: PASS

    Pregunta

    ¿Funciona la ruta GitHub record → publish → gate → dispose contra GitHub real?

    Proposición demostrada

    Sí. Bash + gh + jq pueden crear y conservar la identidad del issue por RUN, publicar intentos inmutables con marcador en la primera línea, leer todos los comentarios paginados, aceptar solo el último marcador válido PASS/scope:OK ligado a la pregunta actual, conservar un parche completo grande mediante comentarios enlazados y copiar Después del PASS o cerrar como NOT_PLANNED.

    Corrección observada a la premisa de tamaño: GitHub aceptó y conservó un comentario de 70 912 caracteres, con parche de 70 206, por gh issue comment. Un segundo envío de 301 372 caracteres, con parche de 300 666, fue rechazado con GraphQL: Body is too long (maximum is 65536 characters) (addComment). El texto del error no demuestra un umbral efectivo exacto de 65 536: la primera aceptación lo contradice. No se afirma haber localizado la frontera exacta. El fallback completo sí quedó ejercitado.

    FAIL, PASS e INCONCLUSIVE de los comentarios son fixtures deliberadas de control del protocolo, no tres conclusiones contradictorias de esta PoC. El veredicto de capacidad es este PASS.

    Ruta ejercitada

    • Repo privado real: pablontiv/dagu-poc-gh-20261014, creado durante esta ejecución; has_issues=true, has_projects=false, has_wiki=false.
    • Sandbox: /tmp/poc-PocGithubWrite-7JCtfT/; estado: $S/state/R1/issue.number=1, $S/state/R2/issue.number=2.
    • Base leída con git rev-parse HEAD: a6fc996ff7b61b1052e322040a37484224a5ef48. Ninguna escritura en ese repositorio local.
    • R1/chore(deps-dev): bump the npm-version-updates group with 2 updates #1: record → FAIL/1 → PASS/2 → ruido cercado → gate PASS → INCONCLUSIVE/3 → gate bloqueado → pregunta editada → PASS viejo distinto → tamaño → fallback → gate bloqueado con PASS obsoleto → copia de Después del PASS al cuerpo.
    • R2/ci: keep skill validation Linux-only #2: record → cierre con comentario VERDICT: FAIL y motivo not planned → lectura stateReason=NOT_PLANNED.
    • Solo se escribieron el repo autorizado y sus issues/comentarios, más archivos del sandbox y este veredicto. No se crearon gists, releases ni assets. No se modificaron ~/.omp, ~/.dagu ni Dagu vivo. Scripts ejecutados con bash real; no se desplegó otro DAG porque las primitivas de Dagu ya estaban probadas.
    • Las URL e IDs siguientes son evidencia histórica; el repo se elimina tras escribir este archivo.

    Intentos

    1. Creación: un fallo observado y una corrección

    Comando inicial de create.sh:

    gh api --method POST user/repos -f name=dagu-poc-gh-20261014 -F private=true -F has_issues=true -F has_projects=false -F has_wiki=false --jq '{full_name,private,has_issues,has_projects,has_wiki}'

    Salida: unexpected end of JSON input, exit 1. Se detuvo el intento. gh api --include repos/pablontiv/dagu-poc-gh-20261014 devolvió HTTP 404: todavía no existía. Se repitió la misma creación con --include y sin --jq para conservar la respuesta HTTP: 201 Created. Una lectura posterior con el filtro anterior confirmó privado/issues/proyectos/wiki. La causa del primer JSON vacío no quedó determinada; no se atribuye falsamente al filtro.

    Descubrimiento de solo lectura: gh repo create --help; mostró --disable-wiki, pero no un flag de creación para proyectos. Por eso la creación usó REST con los cuatro valores explícitos. Preparación: mktemp -d /tmp/poc-PocGithubWrite-XXXXXX.

    2. Record y publicaciones normales

    bash record.sh con el entorno abajo creó #1 y capturó de la URL el número 1. bash publish.sh se ejecutó con VERDICT=FAIL ATTEMPT=1, después VERDICT=PASS ATTEMPT=2 y la misma base SHA.

    • FAIL/1: https://github.com/pablontiv/dagu-poc-gh-20261014/issues/1#issuecomment-6180627623.
    • PASS/2: https://github.com/pablontiv/dagu-poc-gh-20261014/issues/1#issuecomment-6180629843.
    • Cada primera línea fue exactamente el marcador requerido, con scope:OK, run:R1, attempt:1/2 y la base indicada. Ambos incluyeron pregunta, resultado, diffstat y <details> con el falso marcador en un fence.
    • PASS/2 añadió Después del PASS y deliberadamente duplicó espacios y usó CRLF en la pregunta publicada, conservando el texto normalizado.
    • bash noise.sh publicó un comentario adicional cuyo primer renglón era un fence, seguido únicamente del marcador falso PASS run:FAKE attempt:9 base:x: comentario 6180634185. Es posterior al PASS y fuerza una segunda página con per_page=2.

    3. Lectura paginada y controles del gate

    Lecturas exactas en cada ejecución:

    gh issue view "$ISSUE" --repo "$REPO" --json body
    gh api --paginate --slurp "repos/$REPO/issues/$ISSUE/comments?per_page=2"
    1. Primer bash gate.sh: 2 páginas, 3 comentarios, 2 marcadores válidos, último PASS/2 (6180629843), question_matches=true, allow=true, exit 0. El marcador falso más reciente quedó fuera de la selección, al igual que los falsos dentro del parche. La normalización CRLF/whitespace quedó probada con pregunta publicada alterada solo en formato.
    2. VERDICT=INCONCLUSIVE ATTEMPT=3 bash publish.sh creó 6180638818. Gate: 2 páginas, 4 comentarios, 3 válidos, último INCONCLUSIVE/3, question_matches=true, allow=false, exit 1 esperado. No se recurre a un PASS anterior.
    3. bash change-question.sh ejecutó gh issue edit --body añadiendo Ahora también exige una pregunta revisada.. Gate: question_matches=false, allow=false. Comparación independiente del PASS/2 leído con el cuerpo actual: old_pass_matches=false.
    4. Tras el fallback, un gate nuevo leyó 7 páginas, 13 comentarios, 5 válidos, seleccionó el PASS/4 real 6180670136 y bloqueó por question_matches=false, exit 1. Este último control aísla el rechazo de un PASS obsoleto, sin que INCONCLUSIVE sea la razón del bloqueo.

    4. Tamaño: resultado inesperado, rechazo real y fallback

    1. PATCH_LINES=70 bash oversize.sh: parche 70 206, cuerpo 70 912 caracteres; publicación aceptada, comentario 6180650968. Se detuvo y registró esta contradicción de la expectativa. gh api repos/pablontiv/dagu-poc-gh-20261014/issues/comments/6180650968 --jq '{id,characters:(.body|length),url:.html_url}' devolvió characters:70912; no hubo truncado silencioso.
    2. Se parametrizó el generador con PATCH_LINES. PATCH_LINES=300 bash oversize.sh: parche 300 666, cuerpo 301 372; exit 1 con GraphQL: Body is too long (maximum is 65536 characters) (addComment). Este intento no llegó a generar un comentario.
    3. bash fallback.sh: codificó el parche completo en base64, lo dividió en siete payloads de hasta 60 000 caracteres ASCII más metadatos y los publicó como comentarios JSON no-veredicto. IDs: 6180667155, 6180667518, 6180667919, 6180668387, 6180668900, 6180669358, 6180669777.
    4. Publicó al final el marcador PASS/4 con preview de 2 000 caracteres y enlaces a las siete partes: 6180670136. Cuerpo final: 3 553 caracteres.
    5. Releyó cada parte mediante gh api repos/$REPO/issues/comments/$ID, ordenó por index, concatenó los datos y decodificó base64. Salida: original_characters:300666, reread_characters:300666, identical:true. Releyó también el comentario final y contó linked_chunks:7.

    El fallback mínimo dentro de la autorización es usar los propios comentarios del issue; no necesita permisos para gist ni release. Los payloads no empiezan por marcador de veredicto. El comentario que los enlaza se publica último, conserva el resultado y la pregunta, y sirve de punto de entrada durable para reconstruir el parche sin pérdida.

    5. Dispose

    • bash dispose-pass.sh releyó el PASS/2 desde GitHub, extrajo únicamente su sección Después del PASS hasta la siguiente cabecera H2 fuera de fences, la añadió al cuerpo actual con gh issue edit --body y releyó el issue: state:OPEN, stateReason:"", after_pass_copied:true, question_preserved:true. La pregunta revisada se conservó. Esta fue una prueba mecánica de copia; no una promoción autorizada por el PASS obsoleto.
    • RUN=R2 bash record.sh creó ci: keep skill validation Linux-only #2. RUN=R2 VERDICT=FAIL bash dispose-close.sh ejecutó gh issue close --reason "not planned" --comment .... Relectura --json state,stateReason,comments: state:CLOSED, stateReason:NOT_PLANNED, comentario final VERDICT: FAIL; scope:OK; run:R2. Cierre not planned de la fixture de PoC..
    • No se ejecutaron tests, builds, formatters ni linters. Las verificaciones anteriores son las lecturas reales exigidas por esta PoC.

    Implicación para el diseño

    1. La ruta GitHub de §4/§10 funciona con estado por RUN y un marcador exclusivamente en la primera línea. Seleccionar el último marcador válido antes de comprobar PASS evita rescatar un PASS cuando hay un resultado posterior bloqueante.
    2. La pregunta del cuerpo y la del último comentario se comparan por texto normalizado, sin hash. La edición del cuerpo invalida un PASS anterior. Los fences se respetan al extraer secciones; el marcador solo se inspecciona en la primera línea, por lo que un marcador dentro de un fence no participa.
    3. La lectura debe conservar --paginate --slurp y aplanar las páginas. El ejercicio usó per_page=2 para demostrar más de una página.
    4. El límite anunciado por GitHub no debe tomarse como una frontera efectiva demostrada de 65 536. Se requiere conservar el parche completo cuando la publicación normal es rechazada; el fallback probado usa partes JSON/base64 en el mismo issue, más un comentario final con preview y enlaces. La implementación debe conservar el marcador original y asignar un intento nuevo cuando corresponda; aquí PASS/4 identifica el fixture de fallback.
    5. Después del PASS debe copiarse al cuerpo en publish según §11.7; esta PoC ejercitó la misma operación en el paso dispose pedido. La promoción necesita además un PASS vigente: copiar una sección de un PASS histórico no vuelve vigente una pregunta modificada.
    6. stateReason del cierre se lee como NOT_PLANNED; el veredicto textual se conserva en el comentario de cierre, no se confunde con ese enum.

    Entorno e invocaciones exactas

    Los scripts siguientes se escribieron en $S y se ejecutaron con bash; cada invocación es independiente, como un paso Dagu. El exit 1 de un gate bloqueado es intencional. QUESTION se conserva por RUN antes de cualquier publicación.

    export S=/tmp/poc-PocGithubWrite-7JCtfT
    export REPO=pablontiv/dagu-poc-gh-20261014
    export STATE_ROOT="$S/state" SCRIPT_DIR="$S"
    export RUN=R1
    export QUESTION='¿Funciona la ruta GitHub record → publish → gate → dispose contra GitHub real?'
    export BASE_SHA=a6fc996ff7b61b1052e322040a37484224a5ef48
    bash "$S/create.sh"
    bash "$S/record.sh"
    # Escribir state/R1/patch.txt con el payload exacto incluido abajo.
    VERDICT=FAIL ATTEMPT=1 bash "$S/publish.sh"
    VERDICT=PASS ATTEMPT=2 bash "$S/publish.sh"
    bash "$S/noise.sh"
    bash "$S/gate.sh"                       # exit 0
    VERDICT=INCONCLUSIVE ATTEMPT=3 bash "$S/publish.sh"
    bash "$S/gate.sh"                       # exit 1 esperado
    bash "$S/change-question.sh"
    PATCH_LINES=70 bash "$S/oversize.sh"      # aceptado: inesperado; detener y leer
    # Lectura de 6180650968 documentada en Intentos.
    PATCH_LINES=300 bash "$S/oversize.sh"     # exit 1: rechazo real
    bash "$S/fallback.sh"
    bash "$S/gate.sh"                       # exit 1: PASS ligado a pregunta anterior
    bash "$S/dispose-pass.sh"
    RUN=R2 bash "$S/record.sh"
    RUN=R2 VERDICT=FAIL bash "$S/dispose-close.sh"
    # Escribir este archivo de veredicto antes de la eliminación.
    bash "$S/cleanup.sh"
    # Tras registrar la salida de eliminación en este veredicto:
    rm -rf /tmp/poc-PocGithubWrite-7JCtfT

    Payload exacto state/R1/patch.txt (fixture de publicación, no un parche aplicado al repo):

    diff --git a/fixture.txt b/fixture.txt
    new file mode 100644
    --- /dev/null
    +++ b/fixture.txt
    @@ -0,0 +1 @@
    +GitHub route fixture
    <!-- poc-verdict:PASS scope:OK run:FAKE attempt:9 base:x -->
    

    create.sh

    #!/usr/bin/env bash
    set -euo pipefail
    gh api --include --method POST user/repos -f name=dagu-poc-gh-20261014 -F private=true -F has_issues=true -F has_projects=false -F has_wiki=false

    record.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${QUESTION:?}"
    STATE="$STATE_ROOT/$RUN"
    mkdir -p "$STATE"
    printf '%s' "$QUESTION" > "$STATE/question.txt"
    TITLE=$(jq -nr --arg q "$QUESTION" '$q|gsub("\\s+";" ")|"PoC: " + .|.[0:200]')
    printf '## PoC\n\n%s\n' "$QUESTION" > "$STATE/issue-body.txt"
    URL=$(gh issue create --repo "$REPO" --title "$TITLE" --body-file "$STATE/issue-body.txt")
    ISSUE=${URL##*/}
    [[ "$ISSUE" =~ ^[0-9]+$ ]]
    printf '%s\n' "$ISSUE" > "$STATE/issue.number"
    printf 'record run=%s issue=%s url=%s\n' "$RUN" "$ISSUE" "$URL"

    publish.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${VERDICT:?}" "${ATTEMPT:?}" "${BASE_SHA:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    QUESTION=$(cat "$STATE/question.txt")
    {
      printf '<!-- poc-verdict:%s scope:OK run:%s attempt:%s base:%s -->\n' "$VERDICT" "$RUN" "$ATTEMPT" "$BASE_SHA"
      printf '\n## Pregunta\n\n'
      if [[ "$VERDICT" == PASS ]]; then
        jq -nr --arg q "$QUESTION" '"  " + ($q|gsub(" ";"  ")) + "\r\n"'
      else
        printf '%s\n' "$QUESTION"
      fi
      printf '\n## Proposición demostrada\n\nResultado observado: %s.\n' "$VERDICT"
      printf '\n## Ruta ejercitada\n\nGitHub issues mediante bash + gh + jq.\n'
      printf '\n## Comandos y salida\n\ngh issue comment: publicación del intento %s.\n' "$ATTEMPT"
      if [[ "$VERDICT" == PASS ]]; then
        printf '\n## Después del PASS\n\nResultado mantenido: ruta GitHub.\nAlcance: record, publish, gate y dispose.\nAceptación: conservar pregunta y parche y promover solo PASS vigente.\n'
      fi
      printf '\n## Diffstat\n\n```text\nfixture.txt | 1 +\n1 file changed, 1 insertion(+)\n```\n\n'
      printf '<details>\n<summary>Parche de la PoC</summary>\n\n```diff\n'
      cat "$STATE/patch.txt"
      printf '\n```\n</details>\n'
    } > "$STATE/comment-$ATTEMPT.txt"
    URL=$(gh issue comment "$ISSUE" --repo "$REPO" --body-file "$STATE/comment-$ATTEMPT.txt")
    printf '%s\n' "$URL" > "$STATE/comment-$ATTEMPT.url"
    printf 'publish verdict=%s attempt=%s url=%s\n' "$VERDICT" "$ATTEMPT" "$URL"

    noise.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${STATE_ROOT:?}" "${RUN:?}"
    ISSUE=$(cat "$STATE_ROOT/$RUN/issue.number")
    gh issue comment "$ISSUE" --repo "$REPO" --body $'```diff\n<!-- poc-verdict:PASS scope:OK run:FAKE attempt:9 base:x -->\n```'

    sections.jq

    def norm: gsub("\r\n"; "\n") | gsub("\\s+"; " ") | ltrimstr(" ") | rtrimstr(" ");
    def section($heading):
      reduce (gsub("\r\n"; "\n") | split("\n")[]) as $line
        ({fence:null,active:false,done:false,lines:[]};
         if ($line|test("^ {0,3}(`{3,}|~{3,})")) then
           ($line|capture("^ {0,3}(?<f>`{3,}|~{3,})").f) as $f |
           if .fence == null then .fence = $f
           elif ($f[0:1] == .fence[0:1] and ($f|length) >= (.fence|length)) then .fence = null
           else . end |
           if .active then .lines += [$line] else . end
         elif .fence == null and $line == $heading and (.done|not) then .active = true
         elif .fence == null and ($line|test("^## ")) and .active then .active=false | .done=true
         elif .active then .lines += [$line]
         else . end) | .lines | join("\n");
    def marker:
      split("\n")[0] | rtrimstr("\r") |
      capture("^<!-- poc-verdict:(?<verdict>PASS|FAIL|INCONCLUSIVE) scope:(?<scope>OK|FAIL) run:(?<run>[^\\s]+) attempt:(?<attempt>[1-9][0-9]*) base:(?<base>[^\\s]+) -->$");

    gate.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${SCRIPT_DIR:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    gh issue view "$ISSUE" --repo "$REPO" --json body > "$STATE/body.json"
    gh api --paginate --slurp "repos/$REPO/issues/$ISSUE/comments?per_page=2" > "$STATE/pages.json"
    jq -L "$SCRIPT_DIR" -n --slurpfile issue "$STATE/body.json" --slurpfile pages "$STATE/pages.json" '
     include "sections";
     ($issue[0].body | section("## PoC") | norm) as $q |
     [$pages[0][][] | . as $c | (.body | marker) as $m | {comment:$c,marker:$m}] as $valid |
     ($valid[-1] // null) as $last |
     ($last.comment.body // "" | section("## Pregunta") | norm) as $answer |
     {pages:($pages[0]|length), comments:([$pages[0][][]]|length), valid:($valid|length),
      selected:($last.comment.html_url // null), marker:($last.marker // null),
      question:$q, published_question:$answer, question_matches:($q != "" and $q == $answer),
      allow:($last.marker.verdict == "PASS" and $last.marker.scope == "OK" and $q != "" and $q == $answer)}
    ' > "$STATE/gate.json"
    cat "$STATE/gate.json"
    jq -e '.allow' "$STATE/gate.json" > /dev/null

    change-question.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${SCRIPT_DIR:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    QUESTION=$(cat "$STATE/question.txt")
    printf '## PoC\n\n%s Ahora también exige una pregunta revisada.\n' "$QUESTION" > "$STATE/revised-body.txt"
    gh issue edit "$ISSUE" --repo "$REPO" --body "$(cat "$STATE/revised-body.txt")"
    if bash "$SCRIPT_DIR/gate.sh"; then
      printf 'Unexpected gate approval\n' >&2
      exit 1
    fi
    jq -L "$SCRIPT_DIR" -n --slurpfile issue "$STATE/body.json" --slurpfile pages "$STATE/pages.json" '
     include "sections";
     ($issue[0].body | section("## PoC") | norm) as $q |
     [$pages[0][][] | . as $c | (.body|marker) as $m | select($m.verdict=="PASS" and $m.run=="R1") | $c][-1] as $pass |
     {old_pass:$pass.html_url,current_question:$q,old_pass_question:($pass.body|section("## Pregunta")|norm),
      old_pass_matches:($q == ($pass.body|section("## Pregunta")|norm))}
    ' > "$STATE/old-pass.json"
    cat "$STATE/old-pass.json"
    jq -e '.old_pass != null and .old_pass_matches == false' "$STATE/old-pass.json" > /dev/null

    oversize.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    jq -nr --argjson n "${PATCH_LINES:-70}" '"diff --git a/large.txt b/large.txt\n--- /dev/null\n+++ b/large.txt\n" + (("+" + ("x" * 1000) + "\n") * $n)' > "$STATE/full.patch"
    jq -nr --rawfile result "$STATE/comment-2.txt" --rawfile patch "$STATE/full.patch" '($result|split("```diff\n")[0]) + "```diff\n" + $patch + "\n```\n</details>"' > "$STATE/oversized-comment.txt"
    jq -n --rawfile patch "$STATE/full.patch" --rawfile body "$STATE/oversized-comment.txt" '{patch_characters:($patch|length),comment_characters:($body|length)}'
    gh issue comment "$ISSUE" --repo "$REPO" --body-file "$STATE/oversized-comment.txt"

    fallback.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    mkdir -p "$STATE/chunks"
    jq -cn --arg run "$RUN" --rawfile patch "$STATE/full.patch" '
     ($patch|@base64) as $encoded |
     range(0; ($encoded|length); 60000) as $start |
     {kind:"poc-patch-base64",run:$run,attempt:4,index:($start/60000),data:$encoded[$start:$start+60000]}
    ' > "$STATE/chunks.jsonl"
    : > "$STATE/chunk-links.txt"
    while IFS= read -r chunk; do
      INDEX=$(jq -r '.index' <<< "$chunk")
      printf '%s\n' "$chunk" > "$STATE/chunks/chunk-$INDEX.txt"
      URL=$(gh issue comment "$ISSUE" --repo "$REPO" --body-file "$STATE/chunks/chunk-$INDEX.txt")
      ID=${URL##*issuecomment-}
      [[ "$ID" =~ ^[0-9]+$ ]]
      printf -- '- [Parche completo, parte %s](%s)\n' "$INDEX" "$URL" >> "$STATE/chunk-links.txt"
      gh api "repos/$REPO/issues/comments/$ID" > "$STATE/chunks/reread-$INDEX.json"
      printf 'chunk=%s comment=%s\n' "$INDEX" "$ID"
    done < "$STATE/chunks.jsonl"
    jq -nr --rawfile result "$STATE/comment-2.txt" --rawfile patch "$STATE/full.patch" --rawfile links "$STATE/chunk-links.txt" '
     ($result|split("```diff\n")[0]|sub("attempt:2";"attempt:4")) +
     "```diff\n" + $patch[0:2000] + "\n[Vista truncada; parche completo en partes enlazadas]\n```\n</details>\n\n" + $links
    ' > "$STATE/fallback-comment.txt"
    URL=$(gh issue comment "$ISSUE" --repo "$REPO" --body-file "$STATE/fallback-comment.txt")
    printf '%s\n' "$URL" > "$STATE/fallback.url"
    ID=${URL##*issuecomment-}
    gh api "repos/$REPO/issues/comments/$ID" > "$STATE/fallback-reread.json"
    jq -j -s 'map(.body|fromjson)|sort_by(.index)|map(.data)|join("")|@base64d' "$STATE"/chunks/reread-*.json > "$STATE/reassembled.patch"
    jq -e -n --rawfile original "$STATE/full.patch" --rawfile reread "$STATE/reassembled.patch" --slurpfile final "$STATE/fallback-reread.json" '
     {original_characters:($original|length),reread_characters:($reread|length),identical:($original==$reread),
      final_comment:$final[0].html_url,linked_chunks:([$final[0].body|scan("Parche completo, parte")]|length),
      final_characters:($final[0].body|length)} | ., (.identical and .linked_chunks > 0)
    '

    dispose-pass.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${SCRIPT_DIR:?}"
    STATE="$STATE_ROOT/$RUN"
    ISSUE=$(cat "$STATE/issue.number")
    PASS_URL=$(cat "$STATE/comment-2.url")
    PASS_ID=${PASS_URL##*issuecomment-}
    gh api "repos/$REPO/issues/comments/$PASS_ID" > "$STATE/pass-comment.json"
    gh issue view "$ISSUE" --repo "$REPO" --json body > "$STATE/before-dispose.json"
    jq -er -L "$SCRIPT_DIR" -n --slurpfile pass "$STATE/pass-comment.json" --slurpfile issue "$STATE/before-dispose.json" '
     include "sections";
     ($pass[0].body|section("## Después del PASS")) as $after |
     if ($after|norm|length) > 0 then $issue[0].body + "\n\n## Después del PASS\n" + $after
     else error("PASS sin Después del PASS") end
    ' > "$STATE/disposed-body.txt"
    gh issue edit "$ISSUE" --repo "$REPO" --body "$(cat "$STATE/disposed-body.txt")"
    gh issue view "$ISSUE" --repo "$REPO" --json body,state,stateReason > "$STATE/after-dispose.json"
    jq -e -L "$SCRIPT_DIR" -n --slurpfile pass "$STATE/pass-comment.json" --slurpfile issue "$STATE/after-dispose.json" '
     include "sections";
     {state:$issue[0].state,stateReason:$issue[0].stateReason,
      after_pass_copied:(($issue[0].body|section("## Después del PASS")|norm) == ($pass[0].body|section("## Después del PASS")|norm)),
      question_preserved:(($issue[0].body|section("## PoC"))|contains("pregunta revisada"))} | ., (.after_pass_copied and .question_preserved and .state=="OPEN")
    '

    dispose-close.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${RUN:?}" "${STATE_ROOT:?}" "${VERDICT:?}"
    ISSUE=$(cat "$STATE_ROOT/$RUN/issue.number")
    gh issue close "$ISSUE" --repo "$REPO" --reason "not planned" --comment "VERDICT: $VERDICT; scope:OK; run:$RUN. Cierre not planned de la fixture de PoC."
    gh issue view "$ISSUE" --repo "$REPO" --json state,stateReason,comments > "$STATE_ROOT/$RUN/closed.json"
    jq -e --arg verdict "$VERDICT" '{state,stateReason,last_comment:.comments[-1].body} | ., (.state=="CLOSED" and .stateReason=="NOT_PLANNED" and (.last_comment|contains("VERDICT: " + $verdict)))' "$STATE_ROOT/$RUN/closed.json"

    cleanup.sh

    #!/usr/bin/env bash
    set -euo pipefail
    : "${REPO:?}" "${S:?}"
    [[ "$REPO" == pablontiv/dagu-poc-gh-20261014 ]]
    [[ "$S" == /tmp/poc-PocGithubWrite-7JCtfT ]]
    gh repo delete pablontiv/dagu-poc-gh-20261014 --yes
    if gh repo view "$REPO" --json name > "$S/deletion-check.txt" 2>&1; then
      printf 'Repository still viewable after deletion\n' >&2
      exit 1
    else
      STATUS=$?
      cat "$S/deletion-check.txt"
      printf 'gh repo view exit=%s after deletion\n' "$STATUS"
    fi

    Limpieza

    Este archivo se escribió antes de ejecutar gh repo delete pablontiv/dagu-poc-gh-20261014 --yes. La eliminación terminó con exit 0. Inmediatamente después, gh repo view pablontiv/dagu-poc-gh-20261014 --json name terminó con exit 1 y salida:

    GraphQL: Could not resolve to a Repository with the name 'pablontiv/dagu-poc-gh-20261014'. (repository)
    gh repo view exit=1 after deletion
    

    Después se ejecutó rm -rf /tmp/poc-PocGithubWrite-7JCtfT, exit 0, salida Removed /tmp/poc-PocGithubWrite-7JCtfT. El sandbox fue retirado. No se crearon gists; no queda un gist que eliminar. Toda la evidencia y los scripts exactos quedaron conservados en este veredicto.

  3. pablontiv commented on Oct 14, 2026

    @pablontiv
    OwnerAuthor

    VERDICT: PASS

    Pregunta

    ¿La regla positiva «las rutas de tests según la convención del repo y todo *.md pertenecen al cutover» clasifica correctamente los diffs históricos de PoC?

    Proposición demostrada

    En esta muestra, la regla captura todos los cuatro cutovers pedidos (4/4; 100%) y el cutover pi-superpowers #9 (5/5 al ampliarla). Los falsos positivos de capacidad identificados pertenecen a una clase concreta: el archivo clasificado es el artefacto ejecutable o estímulo de la propia prueba, no documentación/tests añadidos para productizarla. Incluye Markdown usado como carga útil de CI/entrega, y un driver E2E situado en test/. Fuera de esa clase no se observó un falso positivo nativo demostrado. La expresión común para Markdown es más simple y completa que la enumeración de documentos por repo de §6.

    PASS es una conclusión acotada a esta muestra, con esa clase identificada, no una garantía universal ni permiso para prohibir experimentos sobre Markdown. Las publicaciones de evidencia de septiembre son otro contexto histórico: eran legítimas bajo la política entonces vigente, pero su escritura en el repo ya no es parte de la PoC actual, cuyo resultado vive en el issue.

    Ruta ejercitada

    Clasificador sobre rutas de diffs reales, usando lectura Git local y GitHub API de solo lectura. Ningún test, build, modificación de repo, acceso al daemon, escritura GitHub o push. Sandbox /tmp/poc-PocDocsTestsRule-LLREkU, eliminado después del veredicto.

    Repos reales:

    • /Users/Shared/harness/a4s
    • /Users/Shared/harness/rootline
    • /Users/Shared/infra/homeserver
    • pablontiv/pi-superpowers por GitHub API; el checkout local vendor/superpowers es otro repo.

    Convenciones derivadas de git ls-tree -r --name-only HEAD para los tres repos locales y del árbol del padre del primer commit PoC para pi-superpowers:

    Repo Convención de tests ejercitada Rutas de tests / archivos del árbol
    a4s componentes de directorio test/ o tests/; sufijos .test.ts / .test.cjs 53/209
    rootline *_test.go; directorio raíz tests/ 280/718
    homeserver componente de directorio tests/ a cualquier profundidad 88/423
    pi-superpowers componente tests/; sufijo .test.ts 14/49

    A cada convención se suma path.endswith('.md'). El conteo no hace distinción de adición, modificación o eliminación: el cutover rootline #350 elimina un test obsoleto, y eso sigue siendo alcance de cutover. Los archivos de PR se leyeron con gh api .../pulls/N/files --paginate, incluyendo previous_filename cuando existe. Para homeserver #359 se aplicó git diff --name-only BASE SHA a cada uno de los cinco snapshots acumulados. No había renames allí. El árbol remoto pi devolvió truncated=false.

    Resultados de PoC

    Diff / unidad Rutas coincidentes / total ¿Bloquearía? Interpretación
    pi-superpowers primer commit d3f6b69fe22a3ad46eddf142611b7c39893fb881 2/3 Sí packages/routing/tests/injection-poc.test.ts, monorepo-layout.test.ts. La inyección nativa está en src/injection-poc.ts, permitido. El historial dice TDD/suite/review durante PoC; bloqueo esperado del acompañamiento de tests, no de su seam nativo. El PR #9 completo NO es este diff: su propio cuerpo dice que dejó de ser PoC 32 commits antes.
    a4s PR #22 2/7 Sí packages/pi-channel-adapter-poc/README.md, test/extension.test.ts. PoC promovida junto con tests/docs; bloqueo esperado de su alcance mixto bajo la regla actual. No se recuperó un snapshot separado previo a esa promoción.
    a4s PR #31 6/38 Sí READMEs y tests de Context Expert. Evidencia histórica explícita de tests/docs antes del live dev-mod; no demuestra que fueran necesarios para probar la capacidad. Diff de PoC/promoción combinado.
    a4s PR #47 1/1 Sí .workspace/docs/research/2026-10-06-evidencia-temporal-ci-documentacion.md. Falso positivo legítimo: el propósito explícito era validar la ruta documental de CI con un único Markdown temporal; no era documentación de un experimento. PR deliberadamente sin merge.
    a4s PR #88 1/6 Sí test/support/pi-atomic-e2e.ts. Artefacto bajo prueba/instrumentación nativa: soporte ejecutable del prototipo E2E que el historial vincula con ejecución Herdr real. El PR también contiene entrega, review y suites: no se afirma que todo su alcance fuera PoC mínimo.
    rootline #346, payload PR #345 4/4 Sí .claude/skills/rootline/SKILL.md, .claude/skills/rootline/ref-query.md, CHANGELOG.md, cmd/rootline/skill_documentation_test.go. Falso positivo legítimo si se escanea la carga útil: la pregunta era el ciclo Dagu de entrega real y estos eran defectos reales usados para ejercitarlo.
    rootline #346, payload PR #348 3/3 Sí CHANGELOG.md, README.md, docs/query.md; mismo caso de carga útil real de entrega.
    rootline #346, payload PR #349 3/3 Sí CHANGELOG.md, cmd/rootline/documentation_contract_test.go, docs/migrate.md; mismo caso de carga útil real de entrega.
    homeserver poc/359-flatcar, snapshot final ad7b3d6c001e3cc48d49f23b7b1a518222b3def9 0/6 No Manifiestos YAML, Compose e Ignition, todos necesarios para la ruta real. También 0 coincidencias en los cuatro snapshots anteriores. El veredicto de capacidad fue FAIL; el scope permitiría correctamente investigar una capacidad que acaba fallando.
    a4s poc/agent-end-result, archivos recuperados del transcript 0/2 No poc/agent-end-probe.ts, poc/drive.py; permitidos. Worktree/branch ya eliminados. No se presenta como diff Git recuperado: los nombres y escrituras se observaron en el transcript original, líneas 687–717.

    8/10 unidades de la tabla dispararían el scope gate; no es una tasa de error. Tres son payloads de una única PoC Dagu, varios son diffs mixtos de promoción, y una es reconstrucción de transcript. El subconjunto exacto de diffs Git/API conserva las mismas conclusiones sin la reconstrucción.

    No se encontró aquí una PoC independiente cuyo objeto fuera crear una nueva SKILL.md; sí una carga útil real que modifica SKILL.md. La extensión a una skill/prompt Markdown como artefacto nativo es [INFERENCE] de la misma clase, no un caso histórico fabricado.

    Cutovers

    Cutover Coincidencias / rutas del diff Positiva contiene el conjunto §6 contiene el conjunto
    homeserver #343 10/25 Sí Sí
    rootline #350 4/19 Sí Sí
    a4s #113 3/9 Sí Sí
    a4s #115 5/12 Sí Sí
    pi-superpowers #9 completo 31/49 Sí Sin regex propuesta para este repo

    Fracción solicitada: 4/4 diffs de cutover contienen rutas del conjunto (100%). Con pi-superpowers #9: 5/5 (100%). No confundir con porcentaje de archivos: 22/65 (33.8%) en los cuatro, o 53/114 (46.5%) al ampliar. Esto no demuestra que cualquier cutover deba necesariamente tocar tests/Markdown; solo mide los históricos pedidos.

    Comparación con §6

    Se aplicaron literalmente los regex de la propuesta, interpretando el escape Markdown \| como alternancia ERE:

    • homeserver: ^(tests/|docs/)|CONTRACT\.md$|^CHANGELOG\.md$|README\.md$
    • rootline: _test\.go$|^tests/|^docs/|^CHANGELOG\.md$|^CONTRIBUTING\.md$|README\.md$|SKILL\.md$
    • a4s: (^|/)test/|\.test\.ts$|README\.md$|^docs/|SKILL\.md$

    En los cuatro cutovers principales los resultados coinciden. Diferencias históricas concretas:

    Los tres últimos son publicación de evidencia después de ejecución, no los comandos del experimento. Bajo la política antigua eran PRs legítimos obligatorios; bajo la regla actual su evidencia se registra fuera del diff PoC. Se muestran aparte, sin sumarlos a la muestra de ejecución.

    La positiva reduce el problema a convención de tests + un único sufijo Markdown. §6 enumera nombres/rutas documentales, tiene huecos observados y requiere completar cada nuevo nombre. Ambas siguen necesitando la convención de tests por repo. La positiva no es matemáticamente un superset en todo el árbol: §6 también bloquea cualquier archivo no-Markdown bajo docs/, mientras la positiva clasifica por tests/Markdown; eso corresponde al cambio de regla propuesto, no a equivalencia total.

    Intentos

    1. mktemp -d /tmp/poc-PocDocsTestsRule-XXXXXX; git -C <repo> remote get-url origin para los cuatro paths inicialmente disponibles. a4s/rootline/homeserver correctos. Primer resultado inesperado: /Users/Shared/vendor/superpowers apunta a pablontiv/superpowers, NO pi-superpowers. Se detuvo esa ruta y se cambió a GitHub API de pi-superpowers; no se clasificó el repo equivocado. Búsqueda opcional glob /Users/Shared/**/pi-superpowers; .../poc-agent-end-result agotó su timeout sin resultados; no se interpretó como ausencia.
    2. git -C <repo> for-each-ref --format='%(refname:short) %(objectname) %(subject)' refs/heads refs/remotes; git -C <repo> worktree list --porcelain, para los tres repos locales. Las ramas PoC indicadas ya no estaban registradas. a4s tenía únicamente el primary registrado; homeserver omp-cross-family-review: las políticas por agente se declaran en settings de OMP #359 se recuperó posteriormente por sus objetos históricos.
    3. git -C <repo> ls-tree -r --name-only HEAD; gh pr view N --repo pablontiv/REPO --json number,title,body,baseRefName,headRefName,headRefOid,commits,files,url. PRs: pi-superpowers 9; a4s 22,31,47,88,113,115; rootline 345,348,349,350; homeserver 343. Salida: árboles/conjuntos de archivos reales; todos los comandos exit 0. Se derivaron las convenciones citadas arriba.
    4. git -C <repo> log --all --format='%H %s' --regexp-ignore-case --grep=poc --grep='proof of concept'; gh issue view 359 --repo pablontiv/homeserver --json body,comments,url,title; mismo comando para rootline 346. Salida: issue omp-cross-family-review: las políticas por agente se declaran en settings de OMP #359 proporciona los cinco snapshots y su base de PoC; fix(omp-dagu): isolate live recovery run identities #346 distingue payloads PR345/348/349 del posterior cutover PR350.
    5. gh api repos/pablontiv/pi-superpowers/commits/d3f6b69fe22a3ad46eddf142611b7c39893fb881; git -C /Users/Shared/infra/homeserver cat-file -e 'ad7b3d6c001e3cc48d49f23b7b1a518222b3def9^{commit}'. Salida: primer commit Pi solo cambia tres archivos, dos tests; objeto homeserver PoC existe (exit 0).
    6. gh api 'repos/pablontiv/pi-superpowers/git/trees/<padre-de-d3f6b69>?recursive=1'; git -C /Users/Shared/infra/homeserver log --format='%H %P %s' ad7b3d6c001e3cc48d49f23b7b1a518222b3def9 -- poc/359; git ... show --format='%H %P %s' --name-status ffb67e02 -- poc/359. Salida: árbol remoto completo, cinco commits PoC consecutivos, base 2b6ef4a6d0be7ce938f68d1f2c8d7e63a25a530d. Lectura del cuerpo docs(readme): clarify config authority #9 confirma separación entre primer commit y entrega formal.
    7. gh api --paginate repos/pablontiv/REPO/pulls/N/files para los PRs del intento 3; git -C /Users/Shared/infra/homeserver diff --name-only 2b6ef4a6d0be7ce938f68d1f2c8d7e63a25a530d SHA, para SHA ffb67e02, 0059e20b, 1c1815a8, 9002e495, ad7b3d6c. Aplicación en memoria de re.search(test_convention, path) or path.endswith('.md') y de §6 sobre los mismos nombres, incluyendo ambos nombres de renames de la API. Salida: tabla; snapshots homeserver 0/2, 0/4, 0/4, 0/5, 0/6. Cutovers 4/4; ampliados 5/5.
    8. gh api repos/pablontiv/homeserver/pulls/N/files para N=221,224,234. Salida: §6 omite los tres documentos de evidencia concretos citados; positiva los captura. Lecturas del transcript a4s .../2026-10-14T06-36-06-114Z_01a13920-1462-7490-a658-113ae39cde03.jsonl:670-718, por ventanas, proporcionan los dos nombres de archivos y la eliminación histórica del worktree. Aplicación del mismo clasificador: 0/2. Totales: 22/65 paths cutover principales, 53/114 ampliados.

    Todos los datos temporales de las llamadas se escribieron exclusivamente en el sandbox. Solo este veredicto se conserva.

    Implicación para el diseño

    Usar la regla positiva como definición de alcance, con la clase explícita «artefacto/estímulo bajo prueba» reconocida en la pregunta. No equivale a que todo archivo .md sea documentación secundaria ni a que todo archivo bajo test/ sea un test de productización. No cambiar el nombre/formato o usar otra ruta para eludir el gate: la PoC debe seguir la ruta real.

    Para rootline #346, separar el diff del orquestador PoC de los diffs de issues reales usados como payload: estos ya recorren feature, con su propio alcance de cutover, y la regla PoC no debe escanearlos como si fueran añadidos de documentación al experimento. Para un CI documental o una skill/prompt nativa, el contenido Markdown puede ser el estímulo mínimo imprescindible. Para #88, distinguir un driver nativo bajo test/ de suites agregadas antes del ejercicio.

    La regla positiva mejora la cobertura de evidencia/documentos como ref-query.md e infrastructure/experiments/.../*.md sin mantener una lista de títulos. El resultado/veredicto de esta PoC queda en este archivo externo; no se generó documentación de repo.

    Fuentes de caso: pi-superpowers #9, primer commit, a4s #22, #31, #47, #88, #113, #115, rootline #346, #345, #348, #349, #350, homeserver #359, #343, #221, #224, #234.

  4. pablontiv commented on Oct 14, 2026

    @pablontiv
    OwnerAuthor

    VERDICT: FAIL

    Pregunta

    ¿Puede un checkpoint determinista del diff detectar nuevas dependencias/paquetes/apps/extensiones en los casos históricos indicados, con ≤10% de falsos positivos?

    Proposición demostrada

    Un detector de adiciones de manifiestos y paquetes detecta homeserver G3 #208 y la adopción MQTT de DSPRIMA, pero omite orca-dispatch-kit PR #14: el CLI inventado se añade como código de un script, sin dependencia declarada ni paquete/app/extensión nuevo.

    Ruta ejercitada

    Solo lectura de git log/show/diff/ls-tree y gh issue/pr; ejecución del detector shell/jq contra BASE_SHA..HEAD históricos. Sandbox: /tmp/poc-PocDependencyNet-q7QUpu.

    Intentos

    1. Descubrimiento: git -C /Users/Shared/dsprima log ... → fatal: not a git repository. Se detuvo ese intento; se localizaron los repos hijos mediante find y se continuó en CuBarcodeNG.
    2. Descubrimiento G3: git show a0d243c3:pyproject.toml → no existía entonces. PoC #204 (no mergear): CLEAN sin check registrado #208 añadió tooling/h0/g3, y el problema requests aparece en la integración posterior (commit correctivo be5de914). Se conserva la frontera de PR PoC #204 (no mergear): CLEAN sin check registrado #208 pedida.
    3. Descubrimiento a4s: gh issue view 50 --json timelineItems → campo no admitido. Repetido con closedByPullRequestsReferences: fix(synagent): support Node 26 in Claude diagnostics generator #50 y Synagent: comandos y eventos de ciclo de vida emitidos por el adaptador, con correlación #126 abiertos y sin PR de cierre.
    4. Detector inicial, bash <sandbox>/dependency-net.sh /Users/Shared/infra/homeserver a0d243c3^ a0d243c3 → package-or-extension tooling/h0, package-or-extension tooling/h0/g3: HIT esperado.
    5. Mismo detector, /Users/Shared/dsprima/CuBarcodeNG c19f500^ c19f500 → DSPRIMA.Platform.Mqtt en Common.csproj, Directory.Packages.props y tests.csproj: HIT esperado. Identidad real: commit c19f500e4eb5df090bd8a30e9c2de0ac61ab24a3, “Unify CuBarcode MQTT on DSPRIMA.Platform.Mqtt 3.0.0”, 2026-10-03.
    6. Mismo detector, /Users/Shared/harness/orca-dispatch-kit cdff3fd^ cdff3fd → salida vacía: MISS inesperado. Se detuvo el intento y se registró antes de seguir. git diff --name-status contiene solamente src/verified-submit.sh y tests/bin/orca, tests/cases/*, tests/run.sh nuevos. La interfaz externa inventada no aparece en un manifiesto ni en un directorio de paquete. No se adapta la regla a nombres del caso.
    7. Negativos, detector inicial, misma invocación para <sha>^..<sha>: los diez homeserver y los primeros cinco a4s no emiten nada; a4s PR refactor(context-expert): unify Pi adapter package #95 (888c1d6) emite pi-ai, pi-coding-agent y pion añadidos a packages/context-expert/package.json. Falso positivo de reutilización/movimiento entre manifiestos. Se detuvo y registró. Iteración: comparar contra el conjunto de dependencias de TODO el árbol base, no solamente el archivo anterior. Esta corrección modela “dependencia nueva para el repo”, sin excepción por caso.
    8. Iteración repo-wide: homeserver 10/10, a4s 10/10 y orca-dispatch-kit 10/10 salen vacíos. DSPRIMA PR fix(dagu): rewind invalid PoC results with actionable diagnostics #316 (211a612, adopción de tipos de contratos compartidos) emite DSPRIMA.Platform.Contracts: falso positivo para capacidad externa nueva. Se detuvo y registró; se mantiene la regla porque efectivamente hay dependencia nueva, aunque no necesariamente requiere una PoC. Se continúa la muestra restante sin cambiar el detector.
    9. Continuación DSPRIMA: PR fix(dagu): recover feature command failures within the same run #315 (37b0473, corrección de warnings) añade Scriban.Signed y se señala: segundo falso positivo semántico (dependencia añadida al reparar tooling E2E). Se detuvo y registró, sin cambiar regla.
    10. Continuación sin cambios: los cinco DSPRIMA restantes salen vacíos. Muestra final: 40 PRs ordinarias, dos señaladas (5% agregado; 20% en DSPRIMA, 0% en los otros tres repos).
    11. Reejecución final del detector repo-wide sobre todos los casos de la tabla siguiente: mismo resultado en los dos hits; el miss conocido de Orca permanece; las cuatro expectativas negativas salen vacías. Todos los comandos terminan con código 0.
    12. Limpieza: rm -r /tmp/poc-PocDependencyNet-q7QUpu → exit 0. El código exacto y la evidencia quedan conservados en este veredicto; ningún repo primario, estado omp/pi/dagu, daemon o recurso GitHub fue modificado.

    Resultado final de casos

    Cada comando es bash dependency-net.sh <repo> <sha>^ <sha>; para commits merge, ^ es el primer padre. Se usa el diff completo de la PR, no un commit intermedio.

    Caso Repo / HEAD Esperado Observado
    homeserver G3 #208 /Users/Shared/infra/homeserver / a0d243c3 hit HIT: paquetes Python tooling/h0 y tooling/h0/g3
    DSPRIMA MQTT TLS /Users/Shared/dsprima/CuBarcodeNG / c19f500 hit HIT: DSPRIMA.Platform.Mqtt
    orca-dispatch-kit PR #14 /Users/Shared/harness/orca-dispatch-kit / cdff3fd hit MISS: interfaz por script/CLI externo sin manifiesto
    homeserver #300, cutover PR #333 homeserver / 0902e7cb sin flag (reuse) Sin flag
    a4s #50, PROXY PR #48 /Users/Shared/harness/a4s / 81c5c83 miss MISS esperado
    a4s #126, PROXY PR #115 a4s / 8e6a22e miss MISS esperado
    Crossplane deletionPolicy, PR #226 homeserver / f9082a4c miss MISS esperado

    Los issues a4s #50 y #126 están abiertos y sin PR de cierre en GitHub. Main autorizó expresamente los proxies: #48 es la implementación de diagnostics que precede el bug Node 26; #115 es el adaptador omp con agent_end. No son verificación del diff de los issues todavía abiertos. PR #226 contiene evidencia real de deletionPolicy: Orphan y re-adopción silenciosa, pero únicamente añade Markdown; sus manifiestos eran desechables y no versionados.

    G3: #208 se detecta por paquetes nuevos, no por requests (no había pyproject.toml). La integración 9ed264ef y la corrección be5de914 son posteriores. El net no demuestra detección de imports faltantes en un paquete existente.

    Negativos: muestra y resultados

    Muestra de PRs ordinarias merged (fixes, mantenimiento, docs, refactors y features del mismo sistema), seleccionadas de log antes de observar resultados. No se ha establecido independencia estadística ni representatividad: homeserver concentra fixes de una misma capacidad; resultado descriptivo.

    Repo PR → HEAD (todos ejecutados como HEAD^..HEAD) Flags finales
    homeserver #361 ede28f87; #358 2b6ef4a6; #357 e28dbe42; #356 cb533bce; #355 060c5585; #354 2af4dc51; #353 7198efb8; #352 4537d390; #349 3b34c717; #347 28e43fef 0/10
    a4s #121 a6fc996; #120 8465e06; #117 954765e; #111 63a84cb; #97 e1caef2; #95 888c1d6; #93 ed4bcd8; #92 90f264b; #91 fd369e1; #89 1a2f172 0/10
    orca-dispatch-kit #52 ade8992; #49 c1fe832; #48 18a431c; #47 164117c; #45 43a83d3; #42 f461e4e; #40 4e01935; #39 e0a0adc; #37 94b7188; #51 1c68a12 0/10
    CuBarcodeNG #320 71b0b78; #319 c6a4398; #318 63aeecb; #316 211a612; #315 37b0473; #314 3b10965; #313 437f3b7; #312 6ca5c8b; #311 87caf2f; #297 2fa15fa 2/10: #316 DSPRIMA.Platform.Contracts; #315 Scriban.Signed

    Ambos falsos positivos son adiciones reales de dependencias: “falso” se refiere a la inferencia de que cada una exige una capacidad-PoC. No se maquillan como negativos limpios. El primer detector por archivo además señalaba la reutilización a4s #95; la comparación repo-wide la elimina.

    Check mínimo ejercitado

    Requiere bash, git y jq. Compara identidades (no versiones); dependencias previamente presentes en cualquier manifiesto base cuentan como reutilización. Añade csproj/props porque el objetivo DSPRIMA es .NET.

    #!/bin/bash
    set -eu
    repo=$1 base=$2 head=$3
    # Emit reasons; checkpoint requires a question-bound PASS when output is nonempty.
    blob() { git -C "$repo" show "$1:$2" 2>/dev/null || true; }
    extract() {
      jq -Rrs --arg path "$1" '
        def matches($r): [scan($r) | if type == "array" then .[0] else . end] | unique;
        if $path | endswith("package.json") then
          (fromjson? // {}) | [.dependencies, .devDependencies, .peerDependencies, .optionalDependencies] | map(. // {} | keys) | add | unique
        elif $path | endswith("go.mod") then
          matches("(?m)^\\s*(?:require\\s+)?([A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+)\\s+v[0-9]")
        elif $path | test("(^|/)requirements[^/]*\\.txt$") then
          matches("(?m)^\\s*([A-Za-z0-9][A-Za-z0-9_.-]*)(?:\\[|[<>=!~; ]|$)")
        elif $path | endswith("pyproject.toml") then
          # PEP 621 dependency arrays and Poetry dependency tables.
          ([scan("(?s)(?:dependencies|optional-dependencies)\\s*=\\s*\\[(.*?)\\]") | .[0] | scan("[\"\u0027]([A-Za-z0-9][A-Za-z0-9_.-]*)") | .[0]] +
           [scan("(?ms)^\\[(?:tool\\.poetry(?:\\.group\\.[^.]+)?\\.dependencies)\\]\\s*([^\\[]*)") | .[0] | scan("(?m)^([A-Za-z0-9_.-]+)\\s*=") | .[0]]) | unique
        elif $path | test("\\.(csproj|props)$") then
          matches("<(?:PackageReference|PackageVersion)\\s+Include=\"([^\"]+)\"")
        elif $path | test("\\.ya?ml$") then
          ([scan("(?m)^\\s*(image|chart|url):\\s*[\"\u0027]?([^\\s\"\u0027]+)") | if .[0] == "image" then .[1] | sub("(@sha256:.*|:[^/:]+)$"; "") else .[1] end] +
           (if $path | endswith("Chart.yaml") then
             [scan("(?ms)^dependencies:\\s*(.*)") | .[0] | scan("(?m)^\\s*-\\s*name:\\s*([^\\s]+)") | .[0]]
            else [] end)) | unique
        else [] end
      '
    }
    base_dependencies=$(
      git -C "$repo" ls-tree -r --name-only "$base" |
      jq -Rr 'select(test("(^|/)(package\\.json|go\\.mod|pyproject\\.toml|requirements[^/]*\\.txt)$|\\.(ya?ml|csproj|props)$"))' |
      while IFS= read -r path; do blob "$base" "$path" | extract "$path"; done |
      jq -s 'add // [] | unique'
    )
    git -C "$repo" diff --name-only "$base" "$head" -- '*package.json' '*go.mod' '*pyproject.toml' '*requirements*.txt' '*.yaml' '*.yml' '*.csproj' '*.props' |
    while IFS= read -r path; do
      before=$base_dependencies
      after=$(blob "$head" "$path" | extract "$path")
      jq -nr --arg path "$path" --argjson before "$before" --argjson after "$after" '$after - $before | .[] | "dependency\t\($path)\t\(.)"'
    done
    dirs() {
      git -C "$repo" ls-tree -r --name-only "$1" | jq -Rrs '
        split("\n") | map(select(length > 0)) | map(
          if test("^(packages|apps|extensions)/[^/]+/") then split("/")[0:2] | join("/")
          elif test("^(\\.pi|\\.omp)/extensions/[^/]+") then split("/")[0:3] | join("/")
          elif endswith("/__init__.py") then sub("/__init__\\.py$"; "")
          elif endswith("/package.json") or endswith("/go.mod") or endswith("/pyproject.toml") then split("/")[0:-1] | join("/")
          else empty end) | unique'
    }
    before=$(dirs "$base")
    after=$(dirs "$head")
    jq -nr --argjson before "$before" --argjson after "$after" '$after - $before | .[] | "package-or-extension\t\(.)"'

    En checkpoint se consumiría la salida de bash dependency-net.sh "$PRIMARY" "$BASE_SHA" HEAD: salida no vacía exige el PASS vigente y ligado a la pregunta que feature ya resolvió; aquí solo se ejercitó el detector, no la validación del PASS.

    Limitaciones del PoC: extracción JSON estructurada, extracción Go/TOML/requirements/YAML/XML léxica. Chart/Flux (chart, url, Chart dependencies) y Compose (image) están cubiertos por esa regla, sin demostrar aquí todas las variantes de sintaxis de esos formatos. Se detectan convenciones packages/apps/extensions, .pi/.omp/extensions y raíces con manifiesto o init.py, no directorios arbitrarios sin identidad de paquete. No se resuelven interpolaciones Compose, includes requirements, llamadas CLI, imports no declarados ni dependencias instaladas por scripts. Comparar nombres repo-wide tampoco detecta una nueva necesidad de instalación por consumidor si el nombre ya existía en otro manifiesto.

    Implicación para el diseño

    El net de dependencias es una señal complementaria; no alcanza el PASS exigido y no sustituye triage ni la review de POC=no. Añadir llamadas externas nuevas sería otra pregunta/otro detector, con falsos positivos propios.

  5. pablontiv commented on Oct 14, 2026

    @pablontiv
    OwnerAuthor

    VERDICT: INCONCLUSIVE (umbral prerregistrado: recall ≥70% con FP ≤30%; obtenido recall 62.5%, FP 12.5%; n=16)

    Pregunta

    ¿Un subagente reviewer con el prompt fijo de triage, ciego al desenlace, habría marcado como PoC los ítems históricos que se implementaron sin PoC y tuvieron corrección tardía?

    Proposición demostrada

    Sobre 16 ítems sin PoC (8 con corrección tardía, 8 controles), el reviewer cambió 6 decisiones a poc=yes: 5 de ellas eran ítems con corrección tardía (precisión 83%) y 1 era un control. No marcó ningún bugfix de control (0 de 5). Es estable: 5 de 5 decisiones idénticas al repetir la ejecución.

    Ruta ejercitada

    • Entradas: cuerpo del issue anterior a la implementación (la primera revisión de userContentEdits o el snapshot Bead original) más los manifiestos en el commit padre (local://poc-review-inputs.jsonl, local://poc-review-provenance.json).
    • 16 subagentes reviewer independientes (Rv00–Rv15) que solo leyeron local://review-in-rvNN.md, sin kind ni desenlace.
    • Repetición sobre 5 ítems (Rr00, Rr01, Rr04, Rr09, Rr15).
    • a4s fix(synagent): support Node 26 in Claude diagnostics generator #50 quedó excluido: el issue nació con el desenlace incluido. Los ítems sin un texto previo a la implementación recuperable también quedaron fuera.

    Intentos

    1. PocReviewOnNo no podía lanzar reviewers (no tiene la herramienta task). Main los lanzó; PocReviewOnNo preparó las entradas ciegas.
    2. Matriz de confusión: TP 5, FN 3, FP 1, TN 7.
    Ítem Tipo Corrección tardía Reviewer
    backscroll#124 NEW_EXTENSION sí yes
    orca-dispatch-kit#1 NEW_EXTENSION sí yes
    orca-dispatch-kit#3 NEW_CAPABILITY sí yes
    a4s#27 NEW_EXTENSION sí yes
    a4s#37 API_CHANGE sí yes
    a4s#104 CHORE sí (estado UNSTABLE y primary real al reutilizar el workflow) no
    backscroll#52 BUGFIX sí (la misma clase de defecto reapareció en otra migración) no
    orca-dispatch-kit#35 NEW_CAPABILITY sí (el escaneo incluyó fixtures de test; external_unknown=no) no
    orca-dispatch-kit#26 NEW_CAPABILITY no (dogfood real en la entrega) yes
    7 controles (5 BUGFIX, homeserver#300, a4s#96) — no no
    1. Repetición: Rr00 no, Rr01 yes, Rr04 yes, Rr09 no, Rr15 no; todas idénticas a la primera pasada.

    Implicación para el diseño

    • Historicamente la sesión decidió "sin PoC" en los 16 casos (por selección del muestreo). El reviewer habría convertido 5 correcciones tardías en PoC, a cambio de 1 PoC innecesaria.
    • Los 3 fallos del reviewer no son errores de comportamiento externo no ejercitado que cambie la ruta. orca#35 es un defecto de alcance interno; backscroll#52 es una clase de defecto recurrente; a4s#104 es la reutilización de un workflow probado en otro repo. Excluir orca#35 por external_unknown=no daría 5 de 7 (71%), pero sería un ajuste posterior al umbral y no cambia el veredicto.
    • No se midió qué respondería la propia sesión con un triage forzado. Históricamente la PoC se omitía, no se decidía. Sigue abierto si el review aporta algo por encima de forzar la pregunta.
  6. pablontiv commented on Oct 15, 2026

    @pablontiv
    OwnerAuthor

    Live acceptance of the remaining #135 criteria

    Executed against the real installed .workspace/dagu/dags/{feature,poc}.yaml, from PRIMARY with DAGU_HOME=~/.dagu, using only trivial shell echo commands in Dagu-created PoC worktrees. All times below are 2026-10-15 UTC. Previously supplied validation and POC=no runs were not repeated; a4s-issue-136-v2 was not operated on.

    Criterion Run ID Result Evidence
    feature triage POC=yes enqueues poc acc135-triage → poc-acc135-triage PASS Triage marker; triage_decision stderr: Enqueued dag-run dag=poc run-id=poc-acc135-triage; child waited at run_poc.
    poc without ISSUE: record before experiment, one push-back, two markers, FAIL closes not planned acc135-iterate PASS #139 created 00:18:00Z, before run_poc at 00:18:02.125Z (I:5). Protected push-back used --expected-iteration 0; next run_poc showed iteration 1. Attempt 1 and attempt 2 are FAIL scope:OK. dispose closed it; API reports state=closed, state_reason=not_planned.
    PASS with promote=yes copies ## Después del PASS, enqueues feature, and triage cites PASS poc-acc135-triage → feature-poc-acc135-triage PASS PASS comment; #141 body contains the copied section. dispose enqueued the full feature contract with that POC_BASIS; live triage prompt: For promotion cite this PASS URL as BASIS: https://github.com/pablontiv/a4s/issues/141#issuecomment-6181427213. Promoted feature ended Aborted, onExit succeeded.
    Markdown diff rejects at scope_gate; failure handler publishes scope:FAIL before cleanup acc135-scope PASS (expected rejection) scope_gate stderr: Cutover path: acc135-scope.md. Failure-handler marker and full Markdown patch, created 00:19:34Z; onFailure started 00:19:33.240Z, then onExit started 00:19:34.703Z and logged Deleted branch poc/acc135-scope (was 106f44c).
    Two PoCs wait simultaneously at run_poc on the shared one-slot a4s queue acc135-iterate, acc135-scope PASS I:5–8: run_poc / waiting at 00:18:02.125Z; S:5–8: run_poc / waiting at 00:18:07.945Z. Both remained waiting until scope resumed at 00:19:33.102Z (S:9); simultaneous live status inspection also showed both run_poc [waiting].
    New dependency at checkpoint_push automatically rewinds feature to triage acc135-dependency PASS Disposable commit e402ede38bdf4aaaf1778a2ff93551d5424d2572 added inert acc135-live-probe in package.json. Detector: dependency package.json acc135-live-probe. Dispatcher: Pushed back human task dependency_review to triage; DAG-run queued for resume. D:17–28 records checkpoint_push → dependency_review → triage; live triage showed iteration 1. Ended Aborted before rebase, gate, push, or pr.
    Final PoC worktree/branch cleanup, local and remote acc135-iterate, acc135-scope, poc-acc135-triage PASS git worktree list --porcelain contained only PRIMARY and the unrelated issue-136 worktree. Local refs/heads/poc/, remote-tracking refs/remotes/origin/poc/, and git ls-remote --heads origin 'poc/*' returned no refs. Disposable acc135/dependency-138 remote branch was deleted. #138, #139, #140, and #141 all report closed / not_planned.

    Exact local evidence

    Log aliases, all under ~/.dagu/logs/:

    • I: poc/dag-run_20261015_001758Z_acc135-iterate/dag-run_20261014.181758.203.acc135-i.log
    • S: poc/dag-run_20261015_001758Z_acc135-scope/dag-run_20261014.181758.219.acc135-s.log
    • D: feature/dag-run_20261015_001816Z_acc135-dependency/dag-run_20261014.181816.226.acc135-d.log
    • Triage dispatch stderr: feature/dag-run_20261015_001816Z_acc135-triage/run_20261015_001845Z_112828/triage_decision.20261014.181845.107.acc135-t.err
    • Scope rejection: poc/dag-run_20261015_001758Z_acc135-scope/run_20261015_001933Z_e3fa9c/scope_gate.20261014.181933.102.acc135-s.err:1
    • Scope cleanup: same attempt directory, onExit.20261014.181934.703.acc135-s.out:1
    • Automatic FAIL closure: poc/dag-run_20261015_001758Z_acc135-iterate/run_20261015_002129Z_e219bd/dispose.20261014.182129.131.acc135-i.err:1
    • Dependency checkpoint SHA: feature/dag-run_20261015_001816Z_acc135-dependency/run_20261015_002114Z_dc2e5f/checkpoint_push.20261014.182114.113.acc135-d.out:2
    • Detector / dispatcher: <PRIMARY>/.git/dagu-feature/runs/acc135-dependency/{dependencies,dependency-transition.log}:1.

    Safety and operational observation

    No acceptance criterion failed. The scope test's DAG failure was the required negative result, and its failure handler published before cleanup.

    Dagu 2.19.2 stop on a settled human-task wait only wrote CANCEL_REQUESTED; it did not transition those feature runs out of Waiting or run cleanup. To finish safely, each was resumed through reviewed POC=no triage solely to obtain an active executor, then stopped via its live socket. Both ended Aborted at triage_decision with onExit succeeded. implement was never completed on the promoted run, and never completed again after the dependency rewind. The dispatch-only acc135-triage naturally ended Succeeded after enqueueing its child, with all implementation steps skipped.

    Only the explicitly required dependency checkpoint_push pushed a disposable branch; it never pushed to main, and that branch was removed afterward. No acceptance run reached the later push or pr steps. PRIMARY remains clean on main at 106f44c. No DAG files were changed. PoC command work was limited to shell builtins, with no real infrastructure, production data, or production configuration involved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions