tk3.biz
Volver al blog

Dividiendo un plan en unidades de trabajo de dev-orchestra e implementándolas en paralelo

Introducción

dev-orchestra 1.2.0 puede dividir un plan en unidades de trabajo. Las unidades que no dependen entre sí se ejecutan en paralelo, cada una en su propio git worktree, y se fusionan en el árbol de trabajo a medida que terminan. Todos los cambios de la v1.1.0 a la v1.2.1 están en el artículo de la versión (en inglés).

La vez anterior probé a pasar instrucciones fijas por rol.

Normalmente, el orquestador (Claude Code u otro agente) ejecuta todos estos pasos por ti. Esta vez, para ver qué ocurre, ejecuté los mismos comandos a mano, uno por uno.

Mi entorno:

  • Windows 11
  • dev-orchestra 1.2.1
  • Claude Code 2.1.309 (el implementer era sonnet de Claude)
  • Python 3.14.7

Para la prueba, apunté DEV_ORCHESTRA_HOME a una carpeta distinta de mi configuración habitual y trabajé en un pequeño repositorio creado para ello (textkit).

dev-orchestra está publicado en GitHub (licencia MIT). Si te parece útil, me alegraría mucho que le dieras una estrella.

→ github.com/istb16/dev-orchestra

Pasos

1. Escribir ## Units en el plan

El ejemplo es un plan para añadir dos funciones, slugify(text) y word_count(text), a un pequeño paquete de Python llamado textkit. Están en archivos distintos, así que se pueden hacer en paralelo. El README y el CHANGELOG describen las dos, así que se actualizan juntos al final.

En el plan (plan.md), añade un encabezado ## Units y un bloque de código yaml.

## Units

```yaml
units:
  - name: slug
    kind: feature
    files: [textkit/slug.py, tests/test_slug.py]
    depends_on: []
  - name: count
    kind: feature
    files: [textkit/count.py, tests/test_count.py]
    depends_on: []
  - name: docs
    kind: integration
    files: [README.md, CHANGELOG.md]
    depends_on: [slug, count]
```

### Unit: slug

1. Add `slugify(text)` in textkit/slug.py: lowercase, runs of non-alphanumerics become one `-`, strip `-` at both ends.
2. Add tests/test_slug.py.
  • Cada unidad lleva un name, un kind (feature, fix, refactor, docs, test, ci o integration), sus files (lo que cambia) y depends_on (las unidades que deben terminar antes)
  • Cada unidad tiene una sección ### Unit: <name> con sus pasos
  • Los archivos que tocan todas las partes, como el CHANGELOG, van a una sola unidad integration que depende de todas las demás

En la práctica, el architect escribe este bloque cuando escribe el plan (cuando el trabajo se reparte entre archivos o el plan tiene más de cuatro pasos). Esta vez lo escribí a mano.

Pon las rutas de ## Files to Modify entre comillas invertidas, como `textkit/slug.py`. Cuando las escribí sin comillas invertidas, units check mostró líneas warning: diciendo que los archivos no estaban en Files to Modify (no lo rechazó).

2. Comprobar con units check que las unidades se pueden ejecutar

Al principio puse README en los files de slug y de count. En ese estado, units check lo rechaza.

$ dev-orchestra units check
.ai/workflows/demo/plan.md: the units cannot be run:
  - slug and count may run in parallel but both list README.md

El código de salida es 2. Dos unidades que pueden ejecutarse en paralelo y citan el mismo archivo chocarían al fusionarse, así que se detiene antes de ejecutar nada. Tras dejar README solo en la unidad docs, pasó.

$ dev-orchestra units check
3 units in 2 waves (implementation.max_parallel: 2):
  wave 1: slug, count
  wave 2: docs

slug y count se ejecutan en paralelo en la primera ola, y docs en la segunda. Cuántas se ejecutan a la vez lo decide implementation.max_parallel (2 por defecto, hasta 8).

3. Ejecutar las pruebas de las unidades después de fusionar

En mi entorno, cada vez que el Claude del implementer intentaba ejecutar un comando de shell, se quedaba esperando aprobación, así que no podía ejecutar las pruebas. (dev-orchestra arranca un implementer de Claude con --permission-mode acceptEdits, así que las ediciones de archivos pasan, pero los comandos de shell necesitan aprobación).

Por eso puse implementation.unit_tests: after_merge, que llegó en la 1.2.1, en el .dev-orchestra.yaml del repositorio.

version: 1
implementation:
  unit_tests: after_merge

Así, cada unidad solo escribe código y pruebas, y las pruebas se ejecutan una sola vez, después de fusionarlo todo. Está pensado para proyectos cuyas pruebas comparten una base de datos o un puerto y chocarían si se ejecutaran en paralelo.

La primera vez lo ejecuté sin este ajuste. El registro de herramientas de una unidad no tenía ningún Write y nunca escribió sus archivos, pero su informe decía que los había creado. units merge mostró merged slug: 0 file(s), así que el número de archivos fusionados es la forma de darse cuenta.

4. Aprobar el plan y ejecutar dos unidades en paralelo

El implementer solo se ejecuta con un plan aprobado. design approve registra la aprobación (normalmente el orquestador te muestra el plan y solo lo ejecuta cuando dices que sí).

$ dev-orchestra design approve
approved .ai/workflows/units/plan.md (sha256 f108913500da) for workflow units

run implementer --unit <name> --detach arranca una unidad en segundo plano. Un solo archivo de petición (implement-request.md) sirve para todas las unidades; la CLI le antepone el nombre de la unidad, sus archivos y su sección del plan.

$ dev-orchestra run implementer --unit slug --prompt-file .ai/workflows/units/execution/implement-request.md --detach
started implementer as job implementer-20261025-165514-ee46f2
$ dev-orchestra run implementer --unit count --prompt-file .ai/workflows/units/execution/implement-request.md --detach
started implementer as job implementer-20261025-165515-d7c62b

units status muestra las dos en running, cada una en su propio worktree. Los worktrees se crearon en una carpeta textkit.units junto al repositorio (rutas parcialmente omitidas).

$ dev-orchestra units status
  slug                 feature    running     job implementer-20261025-165514-ee46f2  base d736c6342cc7  ...\textkit.units\d736c634-slug
  count                feature    running     job implementer-20261025-165515-d7c62b  base daf7aed8edd8  ...\textkit.units\daf7aed8-count
  docs                 integration pending     job -  base -  -
Ready to start: none
Unit tests: after_merge (units skip their tests; run them once on the merged tree)

El prompt que recibió una unidad empezaba con una cabecera como esta (del primer intento).

# Work unit: slug (feature)

This run implements one unit of the plan, and only that unit.
Its files: textkit/slug.py, tests/test_slug.py

5. Recoger la que termine primero con jobs wait --any

Desde la 1.2.0, jobs wait acepta varios ids de trabajo. Con --any, vuelve en cuanto termina cualquiera de ellos (salida resumida).

$ dev-orchestra jobs wait implementer-20261025-165514-ee46f2 implementer-20261025-165515-d7c62b --any
implementer-20261025-165514-ee46f2  RUNNING
  unit:     slug (...\textkit.units\d736c634-slug)
  elapsed:  0m30s
  activity: 9 tool uses, context 51,040 tokens
    +00:15  Write textkit/slug.py
    +00:17  Write tests/test_slug.py
    ...
implementer-20261025-165515-d7c62b  SUCCEEDED
  unit:     count (...\textkit.units\daf7aed8-count)
  activity: 8 tool uses, context 49,706 tokens
    +00:15  Write textkit/count.py
    +00:17  Write tests/test_count.py
    ...

count terminó primero mientras slug seguía en marcha. Los dos eran trabajos pequeños de unos 30 segundos.

6. Devolverlas con units merge

Cada unidad terminada se devuelve al árbol de trabajo con units merge. Fusiona archivo por archivo y nunca toca el index, HEAD ni la rama.

$ dev-orchestra units merge count
merged count: 2 file(s)

Esperé a que slug terminara y la fusioné también.

$ dev-orchestra units merge slug
merged slug: 2 file(s)

$ dev-orchestra units status
  slug                 feature    merged      job implementer-20261025-165514-ee46f2  base d736c6342cc7  -
  count                feature    merged      job implementer-20261025-165515-d7c62b  base daf7aed8edd8  -
  docs                 integration pending     job -  base -  -
Ready to start: docs

Los worktrees de las unidades fusionadas se limpiaron. El árbol de trabajo tenía ahora cuatro archivos nuevos.

$ git status --short
?? tests/test_count.py
?? tests/test_slug.py
?? textkit/count.py
?? textkit/slug.py

7. La unidad de integración se ejecuta allí mismo

Nada puede ejecutarse junto a la unidad docs que queda. En ese caso no se crea ningún worktree y se ejecuta directamente en el árbol de trabajo (la ejecuté sin --detach).

$ dev-orchestra run implementer --unit docs --prompt-file .ai/workflows/units/execution/implement-request.md
docs ユニットを実装しました。変更したのは README.md と CHANGELOG.md だけです。
...
$ dev-orchestra units status
  slug                 feature    merged      ...
  count                feature    merged      ...
  docs                 integration merged      job -  base -  in place
Ready to start: none

(El informe del implementer está en japonés porque mi entorno pide las respuestas en japonés. Dice que solo cambió README.md y CHANGELOG.md). Una unidad que se ejecutó allí mismo cuenta como fusionada al terminar. El README ganó una descripción de las dos funciones, y el CHANGELOG una sección ### Added.

8. Ejecutar las pruebas en el árbol fusionado

Con after_merge, las pruebas se ejecutan una vez al final.

$ python -m unittest
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

budget show muestra que ejecutar tres unidades usó solo un intento del implementer (salida resumida).

$ dev-orchestra budget show
  implementer    1/5 used
  delegated runs 3/40 used
  implementer    pass open: slug, count, docs run (plan f108913500da)

Las unidades de un mismo plan comparten un intento; solo volver a ejecutar una unidad gasta otro. El número de ejecuciones en sí (delegated runs) sí cuenta tres.

9. Cuando una fusión entra en conflicto (esta vez no ocurrió)

Mis fusiones no tuvieron conflictos. Según la referencia, hay conflicto cuando, por ejemplo, el mismo archivo cambió en el árbol de trabajo mientras la unidad se ejecutaba, y entonces:

  • units merge termina con el código 7 y deja unas instrucciones en .ai/execution/units/<name>-conflicts.md
  • El orquestador hace que el review_fixer resuelva el conflicto a partir de esas instrucciones
  • units merge <name> --resolved lo registra como resuelto

No probé un conflicto, así que esto es solo una descripción de cómo funciona.

Resumen

  • Escribir un bloque yaml bajo ## Units en el plan divide la implementación en unidades de trabajo
  • units check rechaza las unidades cuyos archivos se solapan antes de ejecutar nada, y muestra las olas
  • Las unidades independientes se ejecutan en paralelo en git worktrees separados y vuelven archivo por archivo con units merge, que nunca toca el index ni la rama
  • Una unidad que no puede ejecutarse junto a ninguna otra (aquí, la unidad integration) se ejecuta allí mismo
  • jobs wait --any te entrega el trabajo que termine primero
  • Las unidades de un mismo plan comparten un intento del implementer
  • Si tus pruebas no pueden ejecutarse en paralelo, usa implementation.unit_tests: after_merge. Y fíjate en el número de archivos que muestra units merge

La próxima vez mido cuánto tarda dev-orchestra y cuántos hallazgos da cada número de revisores.

GitHub: istb16/dev-orchestra (una estrella siempre se agradece)

Para ver todos los artículos de la serie juntos, usa la lista de artículos de “dev-orchestra”.

Artículos relacionados