@ingeniomaps/cauce 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +25 -3
- package/agents/roles/system/ai-governance-lead/SKILL.md +2 -0
- package/agents/roles/system/ai-governance-lead/learning/sources.yaml +2 -4
- package/agents/roles/system/ai-product-manager/SKILL.md +2 -0
- package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/analytics-engineer/SKILL.md +2 -0
- package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -1
- package/agents/roles/system/backend-engineer/SKILL.md +2 -0
- package/agents/roles/system/backend-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/business-operations-manager/SKILL.md +2 -0
- package/agents/roles/system/business-operations-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/business-strategist/SKILL.md +2 -0
- package/agents/roles/system/business-strategist/learning/sources.yaml +2 -8
- package/agents/roles/system/cloud-architect/SKILL.md +2 -0
- package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/community-manager/SKILL.md +2 -0
- package/agents/roles/system/community-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/content-specialist/SKILL.md +2 -0
- package/agents/roles/system/content-specialist/learning/sources.yaml +2 -8
- package/agents/roles/system/customer-success-manager/SKILL.md +2 -0
- package/agents/roles/system/customer-success-manager/learning/sources.yaml +2 -8
- package/agents/roles/system/customer-support-specialist/SKILL.md +2 -0
- package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -8
- package/agents/roles/system/data-analyst/SKILL.md +2 -0
- package/agents/roles/system/data-analyst/learning/sources.yaml +2 -8
- package/agents/roles/system/data-engineer/SKILL.md +2 -0
- package/agents/roles/system/data-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/data-scientist/SKILL.md +2 -0
- package/agents/roles/system/data-scientist/learning/sources.yaml +2 -4
- package/agents/roles/system/database-administrator/SKILL.md +2 -0
- package/agents/roles/system/database-administrator/learning/sources.yaml +2 -2
- package/agents/roles/system/developer-relations-engineer/SKILL.md +2 -0
- package/agents/roles/system/developer-relations-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/devops-engineer/SKILL.md +2 -0
- package/agents/roles/system/devops-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/engineering-manager/SKILL.md +2 -0
- package/agents/roles/system/engineering-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/financial-controller/SKILL.md +2 -0
- package/agents/roles/system/financial-controller/learning/sources.yaml +2 -8
- package/agents/roles/system/finops-engineer/SKILL.md +83 -0
- package/agents/roles/system/finops-engineer/agents/openai.yaml +4 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/01-apagar-en-produccion.md +10 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/02-ahorro-estimado-como-realizado.md +10 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/03-salto-por-regresion.md +10 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/04-optimizar-contra-la-fiabilidad.md +10 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/05-costos-por-cliente.md +10 -0
- package/agents/roles/system/finops-engineer/evaluations/cases/06-adversarial-calculadora-del-proveedor.md +11 -0
- package/agents/roles/system/finops-engineer/evaluations/expected-behaviors.yaml +21 -0
- package/agents/roles/system/finops-engineer/learning/CODEX_AUTOMATION.md +18 -0
- package/agents/roles/system/finops-engineer/learning/HISTORY.md +4 -0
- package/agents/roles/system/finops-engineer/learning/proposals/_template.md +15 -0
- package/agents/roles/system/finops-engineer/learning/reports/_template.md +14 -0
- package/agents/roles/system/finops-engineer/learning/sources.yaml +27 -0
- package/agents/roles/system/finops-engineer/references/operating-model.md +70 -0
- package/agents/roles/system/frontend-engineer/SKILL.md +2 -0
- package/agents/roles/system/frontend-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/growth-marketer/SKILL.md +79 -0
- package/agents/roles/system/growth-marketer/agents/openai.yaml +4 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/01-gastar-sin-baseline.md +10 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/02-metrica-de-plataforma.md +10 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/03-cortar-experimento.md +10 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/04-promesa-que-el-producto-no-sostiene.md +10 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/05-audiencia-sin-base-legal.md +10 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/06-adversarial-caso-de-exito.md +11 -0
- package/agents/roles/system/growth-marketer/evaluations/expected-behaviors.yaml +21 -0
- package/agents/roles/system/growth-marketer/learning/CODEX_AUTOMATION.md +18 -0
- package/agents/roles/system/growth-marketer/learning/HISTORY.md +4 -0
- package/agents/roles/system/growth-marketer/learning/proposals/_template.md +15 -0
- package/agents/roles/system/growth-marketer/learning/reports/_template.md +14 -0
- package/agents/roles/system/growth-marketer/learning/sources.yaml +27 -0
- package/agents/roles/system/growth-marketer/references/operating-model.md +76 -0
- package/agents/roles/system/implementation-manager/SKILL.md +2 -0
- package/agents/roles/system/implementation-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/legal-counsel/SKILL.md +2 -0
- package/agents/roles/system/legal-counsel/learning/sources.yaml +2 -8
- package/agents/roles/system/machine-learning-engineer/SKILL.md +2 -0
- package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/mlops-engineer/SKILL.md +2 -0
- package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/mobile-engineer/SKILL.md +2 -0
- package/agents/roles/system/mobile-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/partnerships-manager/SKILL.md +2 -0
- package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/people-operations-manager/SKILL.md +2 -0
- package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/privacy-compliance-specialist/SKILL.md +2 -0
- package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +2 -8
- package/agents/roles/system/procurement-manager/SKILL.md +2 -0
- package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/product-manager/SKILL.md +2 -0
- package/agents/roles/system/product-manager/learning/sources.yaml +2 -0
- package/agents/roles/system/product-marketing-manager/SKILL.md +2 -0
- package/agents/roles/system/product-marketing-manager/learning/sources.yaml +2 -8
- package/agents/roles/system/project-manager/SKILL.md +2 -0
- package/agents/roles/system/project-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/qa-engineer/SKILL.md +2 -0
- package/agents/roles/system/qa-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/release-manager/SKILL.md +2 -0
- package/agents/roles/system/release-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/revenue-operations-manager/SKILL.md +2 -0
- package/agents/roles/system/revenue-operations-manager/learning/sources.yaml +2 -4
- package/agents/roles/system/sales-representative/SKILL.md +2 -0
- package/agents/roles/system/sales-representative/learning/sources.yaml +2 -8
- package/agents/roles/system/security-engineer/SKILL.md +2 -0
- package/agents/roles/system/security-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/site-reliability-engineer/SKILL.md +2 -0
- package/agents/roles/system/site-reliability-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/software-architect/SKILL.md +2 -0
- package/agents/roles/system/software-architect/learning/sources.yaml +2 -4
- package/agents/roles/system/solutions-engineer/SKILL.md +2 -0
- package/agents/roles/system/solutions-engineer/learning/sources.yaml +2 -4
- package/agents/roles/system/technical-program-manager/SKILL.md +2 -0
- package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -1
- package/agents/roles/system/technical-writer/SKILL.md +2 -0
- package/agents/roles/system/technical-writer/learning/sources.yaml +2 -4
- package/agents/roles/system/ui-designer/SKILL.md +2 -0
- package/agents/roles/system/ui-designer/learning/sources.yaml +2 -0
- package/agents/roles/system/user-researcher/SKILL.md +2 -0
- package/agents/roles/system/user-researcher/learning/sources.yaml +2 -0
- package/agents/roles/system/ux-designer/SKILL.md +2 -0
- package/agents/roles/system/ux-designer/learning/sources.yaml +2 -0
- package/automatization/workflows/autobuild.js +73 -11
- package/automatization/workflows/team.js +59 -6
- package/engine/agents/catalog.js +45 -20
- package/engine/agents/learning.js +19 -2
- package/engine/cli/ops.js +72 -18
- package/engine/core/manifest.js +54 -0
- package/engine/core/ownership.js +68 -21
- package/engine/teams/registry.js +23 -3
- package/package.json +1 -1
- package/teams/000-template.md +110 -0
- package/teams/README.md +60 -0
- package/teams/system/feasibility-review/WORKFLOW.md +60 -0
- package/teams/system/feasibility-review/team.json +73 -0
- package/teams/system/incident-review/WORKFLOW.md +65 -0
- package/teams/system/incident-review/team.json +72 -0
- package/teams/system/product-development/team.json +71 -17
- package/template/AGENTS.md +11 -0
- package/template/README.md +1 -1
- package/template/organization/roles/README.md +37 -0
- package/template/planning/reports/README.md +11 -0
- package/agents/coordinators/.gitkeep +0 -1
- package/agents/specialists/.gitkeep +0 -1
- package/agents/workflows/.gitkeep +0 -1
|
@@ -10,6 +10,8 @@ Crear claridad y flujo entre equipos para obtener un outcome técnico que los co
|
|
|
10
10
|
## Construir contexto
|
|
11
11
|
|
|
12
12
|
1. Leer `AGENTS.md`, `ops.config.json`, `organization/`, estrategia, roadmap, arquitectura, contratos, decisiones, SLO, políticas y programas previos.
|
|
13
|
+
Leer también `organization/roles/technical-program-manager.md` si existe: son las restricciones reales de
|
|
14
|
+
esta empresa para este cargo.
|
|
13
15
|
2. Definir outcome, beneficios, sponsor, owners, usuarios, alcance/no alcance, restricciones, autoridad, horizonte y criterios de cierre.
|
|
14
16
|
3. Mapear workstreams, sistemas, interfaces, dependencias, secuencia, ambientes, proveedores, equipos, skills y capacidad disponible.
|
|
15
17
|
4. Obtener evidencia de baseline, progreso, calidad, riesgos, incidentes, costos y adopción; distinguir hechos, forecast, supuesto y decisión.
|
|
@@ -6,8 +6,9 @@ rules:
|
|
|
6
6
|
require_corroboration_for_major_change: true
|
|
7
7
|
reject_unsourced_claims: true
|
|
8
8
|
automatic_apply: false
|
|
9
|
+
# El contexto de la empresa no es una fuente de la profesión: vive en
|
|
10
|
+
# organization/roles/technical-program-manager.md dentro de cada instalación.
|
|
9
11
|
sources:
|
|
10
|
-
- {name: Company strategy programs architecture roadmaps decisions policies and approved metrics, url: "local://technical-program-context", tier: company-primary, topics: [strategy, programs, architecture, decisions, metrics]}
|
|
11
12
|
- {name: ISO 21503 programme management, url: "https://www.iso.org/cms/%20render/live/en/sites/isoorg/contents/data/standard/08/28/82868.html", tier: standards-primary, topics: [programme, roles, responsibilities, practices]}
|
|
12
13
|
- {name: ISO 21502 project management, url: "https://committee.iso.org/sites/tc258/home/projects/published/iso-21502.html", tier: standards-primary, topics: [project, adaptive, predictive, hybrid]}
|
|
13
14
|
- {name: ISO 31000 risk management, url: "https://www.iso.org/standard/65694.html", tier: standards-primary, topics: [risk, principles, process]}
|
|
@@ -10,6 +10,8 @@ Ayudar a una audiencia concreta a aprender, completar una tarea, consultar una i
|
|
|
10
10
|
## Construir contexto
|
|
11
11
|
|
|
12
12
|
1. Leer `AGENTS.md`, `ops.config.json`, guías editoriales, glosario, repositorios y documentación existente.
|
|
13
|
+
Leer también `organization/roles/technical-writer.md` si existe: son las restricciones reales de
|
|
14
|
+
esta empresa para este cargo.
|
|
13
15
|
2. Identificar audiencia, objetivo, conocimiento previo, entorno, versión, idioma, canal, owner, sensibilidad y fecha de revisión.
|
|
14
16
|
3. Ubicar fuentes autorizadas: interfaz ejecutable, especificación, tests, configuración, ADR, telemetría, SME y release aprobado.
|
|
15
17
|
4. Clasificar la necesidad como tutorial, guía práctica, referencia o explicación; separar tipos cuando persigan necesidades distintas.
|
|
@@ -6,11 +6,9 @@ rules:
|
|
|
6
6
|
require_corroboration_for_major_change: true
|
|
7
7
|
reject_unsourced_claims: true
|
|
8
8
|
automatic_apply: false
|
|
9
|
+
# El contexto de la empresa no es una fuente de la profesión: vive en
|
|
10
|
+
# organization/roles/technical-writer.md dentro de cada instalación.
|
|
9
11
|
sources:
|
|
10
|
-
- name: Company style glossary product code specs tests and documentation analytics
|
|
11
|
-
url: local://technical-writing-context
|
|
12
|
-
tier: company-primary
|
|
13
|
-
topics: [style, terminology, product, api, operations, docs, feedback]
|
|
14
12
|
- name: Google developer documentation style guide
|
|
15
13
|
url: https://developers.google.com/style
|
|
16
14
|
tier: professional-primary
|
|
@@ -11,6 +11,8 @@ Actuar como responsable del lenguaje visual de la interfaz y su aplicación sist
|
|
|
11
11
|
|
|
12
12
|
1. Localizar la raíz operativa del proyecto.
|
|
13
13
|
2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
|
|
14
|
+
Leer también `organization/roles/ui-designer.md` si existe: son las restricciones reales de
|
|
15
|
+
esta empresa para este cargo.
|
|
14
16
|
3. Leer `organization/company.md`, `organization/product.md` y cualquier guía de marca o design system autorizado.
|
|
15
17
|
4. Leer el flujo y criterios entregados por UX Designer, además de evidencia relevante y restricciones confirmadas.
|
|
16
18
|
5. Inspeccionar componentes, tokens y convenciones existentes antes de crear otros. Separar hechos, restricciones y supuestos. No inventar marca, usuarios, métricas ni evidencia observable.
|
|
@@ -5,6 +5,8 @@ rules:
|
|
|
5
5
|
reject_unsourced_claims: true
|
|
6
6
|
automatic_apply: false
|
|
7
7
|
|
|
8
|
+
# El contexto de la empresa no es una fuente de la profesión: vive en
|
|
9
|
+
# organization/roles/ui-designer.md dentro de cada instalación.
|
|
8
10
|
sources:
|
|
9
11
|
- name: W3C Web Accessibility Initiative
|
|
10
12
|
url: https://www.w3.org/WAI/
|
|
@@ -11,6 +11,8 @@ Actuar como responsable de producir evidencia confiable sobre usuarios. Explicar
|
|
|
11
11
|
|
|
12
12
|
1. Localizar la raíz operativa del proyecto.
|
|
13
13
|
2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
|
|
14
|
+
Leer también `organization/roles/user-researcher.md` si existe: son las restricciones reales de
|
|
15
|
+
esta empresa para este cargo.
|
|
14
16
|
3. Leer `organization/company.md` y `organization/product.md`. Consultar políticas de privacidad, seguridad, accesibilidad y salvaguarda cuando apliquen.
|
|
15
17
|
4. Leer sólo el estado de `planning/` necesario para comprender la decisión que la investigación debe informar.
|
|
16
18
|
5. Clasificar cada entrada como dato original, observación, interpretación, inferencia, supuesto o pregunta.
|
|
@@ -5,6 +5,8 @@ rules:
|
|
|
5
5
|
reject_unsourced_claims: true
|
|
6
6
|
automatic_apply: false
|
|
7
7
|
|
|
8
|
+
# El contexto de la empresa no es una fuente de la profesión: vive en
|
|
9
|
+
# organization/roles/user-researcher.md dentro de cada instalación.
|
|
8
10
|
sources:
|
|
9
11
|
- name: GOV.UK User Research
|
|
10
12
|
url: https://www.gov.uk/service-manual/user-research
|
|
@@ -11,6 +11,8 @@ Actuar como responsable de la estructura y comportamiento de la experiencia. Red
|
|
|
11
11
|
|
|
12
12
|
1. Localizar la raíz operativa del proyecto.
|
|
13
13
|
2. Leer `AGENTS.md`, `ops.config.json` y `organization/README.md` si existen.
|
|
14
|
+
Leer también `organization/roles/ux-designer.md` si existe: son las restricciones reales de
|
|
15
|
+
esta empresa para este cargo.
|
|
14
16
|
3. Leer `organization/company.md`, `organization/product.md` y las reglas de diseño o accesibilidad disponibles.
|
|
15
17
|
4. Consultar evidencia del User Researcher y decisiones aprobadas del Product Manager. Leer el estado de `planning/` necesario.
|
|
16
18
|
5. Separar hechos, evidencia, restricciones, supuestos y decisiones pendientes. No inventar investigación, usuarios, métricas ni evidencia observable.
|
|
@@ -5,6 +5,8 @@ rules:
|
|
|
5
5
|
reject_unsourced_claims: true
|
|
6
6
|
automatic_apply: false
|
|
7
7
|
|
|
8
|
+
# El contexto de la empresa no es una fuente de la profesión: vive en
|
|
9
|
+
# organization/roles/ux-designer.md dentro de cada instalación.
|
|
8
10
|
sources:
|
|
9
11
|
- name: W3C Web Accessibility Initiative
|
|
10
12
|
url: https://www.w3.org/WAI/
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Descubre proyecto, servicios y límites desde ops.config.json; no codifica rutas ni proveedores.
|
|
3
3
|
export const meta = {
|
|
4
4
|
name: 'autobuild',
|
|
5
|
-
description: 'Triage → Pick → Ready → Plan → Build → Review → Verify → QA → Commit → Done',
|
|
5
|
+
description: 'Triage → Pick → Cast → Ready → Plan → Build → Review → Verify → QA → Commit → Done',
|
|
6
6
|
whenToUse: 'Ejecutar un hito aprobado con recuperación por WIP y checkpoint humano entre hitos.',
|
|
7
7
|
phases: [
|
|
8
|
-
'Triage', 'Pick', 'Ready', 'Decompose', 'Plan', 'Critique', 'Build', 'Review',
|
|
8
|
+
'Triage', 'Pick', 'Cast', 'Ready', 'Decompose', 'Plan', 'Critique', 'Build', 'Review',
|
|
9
9
|
'Verify', 'QA', 'Commit', 'Done', 'Closing',
|
|
10
10
|
].map((title) => ({ title, detail: `Fase ${title} del protocolo agnóstico` })),
|
|
11
11
|
}
|
|
@@ -81,6 +81,31 @@ const COMMIT = {
|
|
|
81
81
|
branch: { type: 'string' }, leftovers: { type: 'array', items: { type: 'string' } }, reason: { type: 'string' },
|
|
82
82
|
},
|
|
83
83
|
}
|
|
84
|
+
// Dueño por defecto de cada fase. Es determinista: no hace falta preguntarle a un modelo quién
|
|
85
|
+
// revisa la arquitectura o quién decide si la evidencia de calidad alcanza.
|
|
86
|
+
const OWNERS = {
|
|
87
|
+
ready: 'product-manager',
|
|
88
|
+
plan: 'software-architect',
|
|
89
|
+
review: 'software-architect',
|
|
90
|
+
verify: 'qa-engineer',
|
|
91
|
+
qa: 'qa-engineer',
|
|
92
|
+
commit: 'release-manager',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CAST_RULES = 'Elegís quién trabaja, no qué se hace. No inventes slugs: usá sólo los que ' +
|
|
96
|
+
'devuelve el CLI. Un cargo se suma por riesgo, plataforma o alcance, nunca por rutina.'
|
|
97
|
+
|
|
98
|
+
const CAST = {
|
|
99
|
+
type: 'object', additionalProperties: false, required: ['build'],
|
|
100
|
+
properties: {
|
|
101
|
+
build: { type: 'string' },
|
|
102
|
+
review: { type: 'array', items: { type: 'string' } },
|
|
103
|
+
verify: { type: 'array', items: { type: 'string' } },
|
|
104
|
+
qa: { type: 'array', items: { type: 'string' } },
|
|
105
|
+
reason: { type: 'string' },
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
const CONTRACT = {
|
|
85
110
|
type: 'object', additionalProperties: false,
|
|
86
111
|
required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts'],
|
|
@@ -164,11 +189,41 @@ while (safety++ < 50) {
|
|
|
164
189
|
const direct = planning.lane === 'directo'
|
|
165
190
|
const lite = planning.lane === 'lite'
|
|
166
191
|
|
|
192
|
+
// Quién ejecuta cada fase. Los dueños por defecto son fijos y no gastan una llamada; los
|
|
193
|
+
// condicionales entran por riesgo, plataforma y alcance —nunca por rutina—, que es la misma
|
|
194
|
+
// regla que ya usa el team. En `directo` no hay condicionales: el lane baja ceremonia.
|
|
195
|
+
const cast = { ...OWNERS, build: '' }
|
|
196
|
+
if (!direct) {
|
|
197
|
+
phase('Cast')
|
|
198
|
+
const chosen = await read(
|
|
199
|
+
`${CAST_RULES}\n\nTarea ${task.id} en ${task.service}. Aceptación: ${task.acceptance}.\n` +
|
|
200
|
+
`Run "node tools/ops.js agents list ${ROOT} --json" and choose only from the slugs it lists.\n` +
|
|
201
|
+
`Elegí el cargo que implementa según la plataforma del servicio${lite ? '.' : ', y los cargos ' +
|
|
202
|
+
'condicionales que la superficie realmente justifica: seguridad si toca autenticación, permisos, ' +
|
|
203
|
+
'criptografía o datos sensibles; privacidad si toca datos personales; sre si toca disponibilidad, ' +
|
|
204
|
+
'límites o despliegue; ux si cambia una superficie que usa una persona; y el que corresponda a ' +
|
|
205
|
+
'datos o modelos si los toca. Dejá vacío lo que no aplique: sumar un cargo que no aporta es ruido ' +
|
|
206
|
+
'que diluye la revisión.'}`,
|
|
207
|
+
{ schema: CAST, label: 'cast' },
|
|
208
|
+
)
|
|
209
|
+
if (chosen) {
|
|
210
|
+
cast.build = chosen.build || ''
|
|
211
|
+
for (const key of ['review', 'verify', 'qa']) {
|
|
212
|
+
cast[key] = [OWNERS[key], ...(lite ? [] : chosen[key] || [])].filter(Boolean).join(', ')
|
|
213
|
+
}
|
|
214
|
+
log(`Cargos: build=${cast.build || '(sin asignar)'} · review=${cast.review} · qa=${cast.qa}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const asRole = (slugs) => (slugs
|
|
218
|
+
? `Actuá como ${slugs}, respetando el contrato de cada uno en su SKILL.md bajo agents/ y sus ` +
|
|
219
|
+
`límites: un cargo que no puede decidir solo, no decide solo.\n\n`
|
|
220
|
+
: '')
|
|
221
|
+
|
|
167
222
|
if (!planning.wipActive) {
|
|
168
223
|
phase('Ready')
|
|
169
224
|
const ready = await read(
|
|
170
|
-
|
|
171
|
-
|
|
225
|
+
`${asRole(OWNERS.ready)}Check concrete acceptance, dependencies and unresolved decisions for ${task.id}: ` +
|
|
226
|
+
`${task.acceptance}. Clarify wording only; never expand scope.`, { schema: READY },
|
|
172
227
|
)
|
|
173
228
|
if (!ready.ready) {
|
|
174
229
|
await write(`Register ${task.id} in ${HUMAN} with reason and an exact human action: ${ready.reason}.`)
|
|
@@ -192,7 +247,8 @@ while (safety++ < 50) {
|
|
|
192
247
|
|
|
193
248
|
phase('Plan')
|
|
194
249
|
let plan = await run(
|
|
195
|
-
|
|
250
|
+
`${asRole(OWNERS.plan)}Inspect real code, repository instructions, neighbouring conventions, epic context ` +
|
|
251
|
+
`and git status for ${task.id}. ` +
|
|
196
252
|
`Produce the smallest plan satisfying ${task.acceptance}. Planning files cannot be implementation files.`, { schema: PLAN },
|
|
197
253
|
)
|
|
198
254
|
if (!direct && !lite) {
|
|
@@ -209,13 +265,16 @@ while (safety++ < 50) {
|
|
|
209
265
|
}
|
|
210
266
|
await write(
|
|
211
267
|
`Persist active WIP before code: task=${task.id}, hito=${JSON.stringify(task.hito)}, phase=Build, service=${task.service}, ` +
|
|
212
|
-
`acceptance=${JSON.stringify(task.acceptance)}, unchecked steps=${JSON.stringify(plan.steps)}.
|
|
268
|
+
`acceptance=${JSON.stringify(task.acceptance)}, unchecked steps=${JSON.stringify(plan.steps)}. ` +
|
|
269
|
+
`Registrá además el reparto de cargos ${JSON.stringify(cast)} en las decisiones del WIP, para que ` +
|
|
270
|
+
`después se pueda auditar quién revisó qué. Follow the WIP contract exactly.`,
|
|
213
271
|
)
|
|
214
272
|
}
|
|
215
273
|
|
|
216
274
|
phase('Build')
|
|
217
275
|
const build = await run(
|
|
218
|
-
|
|
276
|
+
`${asRole(cast.build)}Implement only ${task.id} inside ${task.service}. Resume at the first pending WIP ` +
|
|
277
|
+
`step; verify completed steps on disk ` +
|
|
219
278
|
`and tick each successful step. Use RED/GREEN for behavior. Acceptance: ${task.acceptance}.`, {
|
|
220
279
|
schema: { type: 'object', required: ['completed', 'summary'], properties: {
|
|
221
280
|
completed: { type: 'boolean' }, summary: { type: 'string' }, blockers: { type: 'array', items: { type: 'string' } },
|
|
@@ -227,7 +286,8 @@ while (safety++ < 50) {
|
|
|
227
286
|
if (!direct) {
|
|
228
287
|
phase('Review')
|
|
229
288
|
let review = await run(
|
|
230
|
-
|
|
289
|
+
`${asRole(cast.review)}Review the actual diff for acceptance, regressions, security, architecture, ` +
|
|
290
|
+
`generated code, migrations and accidental scope. Cada cargo revisa su dominio, no el ajeno.`,
|
|
231
291
|
{ schema: DECISION },
|
|
232
292
|
)
|
|
233
293
|
if (!review.approved) {
|
|
@@ -239,14 +299,15 @@ while (safety++ < 50) {
|
|
|
239
299
|
|
|
240
300
|
phase('Verify')
|
|
241
301
|
const verified = await run(
|
|
242
|
-
|
|
302
|
+
`${asRole(cast.verify)}Discover and run the real gates for ${task.service}: repository instructions first, ` +
|
|
303
|
+
`then applicable test, lint, ` +
|
|
243
304
|
`typecheck and build. Read actual exit codes. passed=true needs commands and no task-caused regression.`, { schema: VERIFY },
|
|
244
305
|
)
|
|
245
306
|
if (!verified.passed || !verified.commands.length) return stop('verify-failed', verified.details)
|
|
246
307
|
|
|
247
308
|
phase('QA')
|
|
248
309
|
const qa = await run(
|
|
249
|
-
`${direct || lite ? 'Perform the cheapest real acceptance check' : 'Exercise real consumer-visible behavior'} for ` +
|
|
310
|
+
`${asRole(cast.qa)}${direct || lite ? 'Perform the cheapest real acceptance check' : 'Exercise real consumer-visible behavior'} for ` +
|
|
250
311
|
`${task.id}. Unit tests alone are not QA. Start only minimum runtime and tear it down. Acceptance: ${task.acceptance}.`,
|
|
251
312
|
{ schema: QA },
|
|
252
313
|
)
|
|
@@ -254,7 +315,8 @@ while (safety++ < 50) {
|
|
|
254
315
|
|
|
255
316
|
phase('Commit')
|
|
256
317
|
const commit = contract.commitPerTask ? await run(
|
|
257
|
-
|
|
318
|
+
`${asRole(OWNERS.commit)}Find the git repo owning ${task.service}, inspect status/diff, stage explicit task ` +
|
|
319
|
+
`files, create one Conventional ` +
|
|
258
320
|
`Commit with footer "Task: ${task.id}", then verify log/status. Never amend or push; report unrelated leftovers.`,
|
|
259
321
|
{ schema: COMMIT },
|
|
260
322
|
) : { committed: true, reason: 'runner.commitPerTask is disabled' }
|
|
@@ -20,14 +20,27 @@ const P = `${ROOT}/planning`
|
|
|
20
20
|
const ROADMAP = `${P}/roadmap`
|
|
21
21
|
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
22
22
|
const INBOX = `${P}/INBOX.md`
|
|
23
|
+
const REPORTS = `${P}/reports`
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Tres formas de invocarlo, porque escribir JSON en un slash command no es razonable:
|
|
26
|
+
//
|
|
27
|
+
// /team quiero cobrar con tarjeta equipo por defecto
|
|
28
|
+
// /team feasibility-review: quiero cobrar con tarjeta equipo elegido por prefijo
|
|
29
|
+
// /team {"team": "acme-soporte", "intent": "..."} argumentos estructurados
|
|
30
|
+
//
|
|
31
|
+
// El prefijo se toma como candidato y se confirma más abajo contra los equipos que existen: si no es
|
|
32
|
+
// uno, el texto completo era la intención y nadie tuvo que aprenderse una sintaxis.
|
|
33
|
+
const input = typeof args === 'string' ? { intent: args } : (args || {})
|
|
34
|
+
const raw = String(input.intent || process.env.OPS_INTENT || '').trim()
|
|
35
|
+
const prefix = raw.match(/^([a-z][a-z0-9-]*)\s*:\s*(.+)$/s)
|
|
36
|
+
const CANDIDATE = String(input.team || process.env.OPS_TEAM || (prefix ? prefix[1] : '') || 'product-development')
|
|
37
|
+
const INTENT = (input.team || !prefix ? raw : prefix[2]).trim()
|
|
26
38
|
|
|
27
39
|
const MANIFEST = {
|
|
28
40
|
type: 'object', additionalProperties: false, required: ['name', 'purpose', 'stages', 'guardrails'],
|
|
29
41
|
properties: {
|
|
30
42
|
name: { type: 'string' }, purpose: { type: 'string' },
|
|
43
|
+
outcome: { type: 'string', enum: ['epic', 'report'] },
|
|
31
44
|
entryAgent: { type: 'string' }, facilitator: { type: 'string' },
|
|
32
45
|
guardrails: { type: 'array', items: { type: 'string' } },
|
|
33
46
|
owners: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -35,6 +48,7 @@ const MANIFEST = {
|
|
|
35
48
|
} } },
|
|
36
49
|
stages: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
37
50
|
id: { type: 'string' }, agent: { type: 'string' }, exitGate: { type: 'string' },
|
|
51
|
+
phase: { type: 'string', enum: ['discovery', 'delivery'] },
|
|
38
52
|
produces: { type: 'array', items: { type: 'string' } },
|
|
39
53
|
} } },
|
|
40
54
|
},
|
|
@@ -75,10 +89,27 @@ const BASE = `Nunca inventes clientes, métricas, restricciones ni decisiones. N
|
|
|
75
89
|
`haría falta averiguar. No promuevas trabajo al BACKLOG, no escribas en sistemas externos y no ` +
|
|
76
90
|
`declares validado nada sin evidencia observable.`
|
|
77
91
|
|
|
92
|
+
const resolved = await agent(
|
|
93
|
+
`${BASE}\n\nRun "node tools/ops.js team list" from ${ROOT}. Report whether "${CANDIDATE}" is one of ` +
|
|
94
|
+
`the slugs it printed, and the full list. Do not guess: report only what the command printed.`,
|
|
95
|
+
{ schema: { type: 'object', required: ['exists', 'teams'], properties: {
|
|
96
|
+
exists: { type: 'boolean' }, teams: { type: 'array', items: { type: 'string' } },
|
|
97
|
+
} }, label: 'team-resolve' },
|
|
98
|
+
)
|
|
99
|
+
if (!resolved) return stop('teams-unavailable', 'no se pudo listar los equipos')
|
|
100
|
+
if (!resolved.exists && (input.team || process.env.OPS_TEAM)) {
|
|
101
|
+
return stop('equipo-inexistente', `${CANDIDATE} no existe. Disponibles: ${resolved.teams.join(', ')}`)
|
|
102
|
+
}
|
|
103
|
+
// El prefijo era parte de la intención, no un equipo: se recompone y sigue con el equipo por defecto.
|
|
104
|
+
const TEAM = resolved.exists ? CANDIDATE : 'product-development'
|
|
105
|
+
const GOAL = resolved.exists ? INTENT : raw
|
|
106
|
+
if (!resolved.exists && prefix) log(`"${CANDIDATE}" no es un equipo: se toma el texto completo como intención.`)
|
|
107
|
+
|
|
78
108
|
const contract = await agent(
|
|
79
109
|
`${BASE}\n\nRun "node tools/ops.js team show ${TEAM} --json" from ${ROOT} and report only what it ` +
|
|
80
|
-
`printed: name, purpose, entryAgent, facilitator, guardrails, the stages with id,
|
|
81
|
-
`
|
|
110
|
+
`printed: name, purpose, outcome, entryAgent, facilitator, guardrails, the stages with id, phase, ` +
|
|
111
|
+
`agent, ` +
|
|
112
|
+
`produces and exitGate, and decisionOwners flattened into owners as domain/agent pairs. Then read ` +
|
|
82
113
|
`${ROOT}/organization/ and report nothing from it: it is context for later stages, not output.`,
|
|
83
114
|
{ schema: MANIFEST, label: 'team-contract' },
|
|
84
115
|
)
|
|
@@ -88,13 +119,17 @@ const owners = (contract.owners || []).map((owner) => `${owner.domain}=${owner.a
|
|
|
88
119
|
const RULES = `${BASE}\n\nEquipo ${contract.name}: ${contract.purpose}\n` +
|
|
89
120
|
`Guardrails: ${contract.guardrails.join(' ')}\n` +
|
|
90
121
|
`${owners ? `Dueños de decisión: ${owners}. Ningún otro cargo resuelve en su dominio.\n` : ''}` +
|
|
91
|
-
`Contexto de la empresa en ${ROOT}/organization/. Intención a evaluar: ${
|
|
122
|
+
`Contexto de la empresa en ${ROOT}/organization/. Intención a evaluar: ${GOAL}`
|
|
92
123
|
|
|
93
124
|
phase('Stages')
|
|
94
125
|
|
|
95
126
|
const handoffs = []
|
|
96
127
|
const blocked = []
|
|
97
|
-
|
|
128
|
+
// Sólo descubrimiento: este recorrido propone trabajo, no lo ejecuta. Las etapas de entrega las
|
|
129
|
+
// corre `autobuild`, y sólo después de que una persona promueva la épica al BACKLOG.
|
|
130
|
+
const discovery = contract.stages.filter((stage) => stage.phase === 'discovery')
|
|
131
|
+
if (!discovery.length) return stop('sin-descubrimiento', `${TEAM} no declara etapas de discovery`)
|
|
132
|
+
for (const stage of discovery) {
|
|
98
133
|
const previous = handoffs.length
|
|
99
134
|
? `Handoffs previos:\n${handoffs.map((entry) => `- ${entry.id}: ${entry.findings}`).join('\n')}`
|
|
100
135
|
: 'Sos la primera etapa: no hay handoff previo.'
|
|
@@ -129,6 +164,24 @@ if (blocked.length) {
|
|
|
129
164
|
|
|
130
165
|
phase('Draft')
|
|
131
166
|
|
|
167
|
+
// Un recorrido que registra lo aprendido no propone trabajo: deja el informe y las tareas de
|
|
168
|
+
// seguimiento en el INBOX, donde una persona decide si alguna merece convertirse en épica.
|
|
169
|
+
if (contract.outcome === 'report') {
|
|
170
|
+
const report = await agent(
|
|
171
|
+
`${RULES}\n\nHandoffs completos:\n${JSON.stringify(handoffs)}\n\nEscribí el informe en ${REPORTS} como ` +
|
|
172
|
+
`<AAAA-MM-DD>-<slug>.md: qué pasó, qué se sabe con evidencia, qué se supone, qué se decidió y qué ` +
|
|
173
|
+
`queda abierto. Separá causa de síntoma y no atribuyas responsabilidad a personas. Registrá cada ` +
|
|
174
|
+
`seguimiento en la sección Lecciones de ${INBOX}, sin promoverlo, y toda acción que requiera una ` +
|
|
175
|
+
`persona en ${HUMAN}.`,
|
|
176
|
+
{ schema: { type: 'object', required: ['file', 'followUps'], properties: {
|
|
177
|
+
file: { type: 'string' }, followUps: { type: 'integer' }, summary: { type: 'string' },
|
|
178
|
+
} }, label: 'report-write' },
|
|
179
|
+
)
|
|
180
|
+
if (!report) return stop('report-unavailable', 'el informe no devolvió resultado')
|
|
181
|
+
log(`Informe en ${report.file}. ${report.followUps} seguimiento(s) en el INBOX, sin promover.`)
|
|
182
|
+
return finish({ team: TEAM, stages: handoffs.length, report: report.file, promoted: false })
|
|
183
|
+
}
|
|
184
|
+
|
|
132
185
|
const epic = await agent(
|
|
133
186
|
`${RULES}\n\nHandoffs completos:\n${JSON.stringify(handoffs)}\n\nComo product-manager, decidí si la ` +
|
|
134
187
|
`intención es viable con la evidencia reunida. Si lo es, redactá la épica: título, slug en ` +
|
package/engine/agents/catalog.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// Resolución única del catálogo de cargos. La usan el ciclo de aprendizaje, la validación de
|
|
4
|
-
// equipos y la instalación en runners
|
|
4
|
+
// equipos y la instalación en runners.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// El catálogo se reparte en dos lugares según de quién es cada cosa:
|
|
7
7
|
//
|
|
8
|
-
// agents/<tipo>/system/<slug>/
|
|
9
|
-
// agents/<tipo>/<slug>/
|
|
8
|
+
// <paquete>/agents/<tipo>/system/<slug>/ viene con Cauce, se actualiza con la dependencia
|
|
9
|
+
// <proyecto>/agents/<tipo>/<slug>/ es de la empresa y manda sobre el anterior
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Los cargos del sistema no se copian al proyecto: evolucionan como profesión y esa evolución es
|
|
12
|
+
// la misma para todos. Lo que sí es de cada empresa —su contexto— vive en `organization/roles/`.
|
|
13
|
+
//
|
|
14
|
+
// Un slug repetido entre tipos distintos sigue siendo ambiguo: ahí no hay ninguna regla que diga
|
|
15
|
+
// cuál gana, y elegir en silencio sería peor que fallar.
|
|
13
16
|
|
|
14
17
|
const fs = require('node:fs')
|
|
15
18
|
const path = require('node:path')
|
|
@@ -24,44 +27,66 @@ function directories(dir) {
|
|
|
24
27
|
} catch { return [] }
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// Dónde está el catálogo que trae Cauce. Se reconoce por tener `roles/system/`, que es el espacio
|
|
31
|
+
// del sistema y no algo que un proyecto deba crear.
|
|
32
|
+
function systemCatalog(root) {
|
|
33
|
+
return require('../core/ownership').packageDir(root, 'agents')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projectCatalog(root) {
|
|
37
|
+
return path.join(root, 'agents')
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
function types(root) {
|
|
28
|
-
|
|
41
|
+
const own = directories(projectCatalog(root))
|
|
42
|
+
const system = directories(systemCatalog(root) || path.join(root, '\0'))
|
|
43
|
+
return [...new Set([...own, ...system])]
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
// Todos los cargos visibles: los
|
|
46
|
+
// Todos los cargos visibles: los de la empresa ocultan a los del sistema con el mismo slug.
|
|
32
47
|
function list(root) {
|
|
33
|
-
const
|
|
48
|
+
const system = systemCatalog(root)
|
|
34
49
|
const found = new Map()
|
|
35
50
|
for (const type of types(root)) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
// El sistema primero, para que el proyecto lo sobrescriba al pasar por encima.
|
|
52
|
+
const sources = [
|
|
53
|
+
system ? [path.join(system, type, 'system'), true] : null,
|
|
54
|
+
[path.join(projectCatalog(root), type), false],
|
|
55
|
+
].filter(Boolean)
|
|
56
|
+
for (const [source, fromSystem] of sources) {
|
|
39
57
|
for (const slug of directories(source)) {
|
|
40
58
|
if (slug === 'system' || !SLUG.test(slug)) continue
|
|
41
59
|
const dir = path.join(source, slug)
|
|
42
60
|
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) continue
|
|
43
|
-
found.set(slug, { slug, type, dir, system })
|
|
61
|
+
found.set(slug, { slug, type, dir, system: fromSystem })
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
return [...found.values()].sort((left, right) => left.slug.localeCompare(right.slug))
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
function
|
|
68
|
+
function find(root, slug) {
|
|
51
69
|
if (!SLUG.test(slug || '')) throw new Error(`agente inválido: ${slug || '(vacío)'}`)
|
|
70
|
+
const system = systemCatalog(root)
|
|
52
71
|
const matches = []
|
|
53
72
|
for (const type of types(root)) {
|
|
54
|
-
const own = path.join(root,
|
|
55
|
-
const
|
|
73
|
+
const own = path.join(projectCatalog(root), type, slug)
|
|
74
|
+
const shipped = system ? path.join(system, type, 'system', slug) : ''
|
|
56
75
|
// Dentro de un tipo la precedencia está definida; entre tipos no, y por eso se acumulan.
|
|
57
|
-
if (fs.existsSync(path.join(own, 'SKILL.md'))) matches.push(own)
|
|
58
|
-
else if (fs.existsSync(path.join(
|
|
76
|
+
if (fs.existsSync(path.join(own, 'SKILL.md'))) matches.push({ dir: own, system: false })
|
|
77
|
+
else if (shipped && fs.existsSync(path.join(shipped, 'SKILL.md'))) {
|
|
78
|
+
matches.push({ dir: shipped, system: true })
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
if (!matches.length) throw new Error(`no existe agents/<tipo>/${slug}/SKILL.md`)
|
|
61
82
|
if (matches.length > 1) {
|
|
62
|
-
throw new Error(`agente ambiguo ${slug}: ${matches.map((
|
|
83
|
+
throw new Error(`agente ambiguo ${slug}: ${matches.map((entry) => entry.dir).join(', ')}`)
|
|
63
84
|
}
|
|
64
85
|
return matches[0]
|
|
65
86
|
}
|
|
66
87
|
|
|
67
|
-
|
|
88
|
+
function resolve(root, slug) {
|
|
89
|
+
return find(root, slug).dir
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { find, list, projectCatalog, resolve, systemCatalog, types }
|
|
@@ -19,11 +19,28 @@ function agentRoot(root, agent) {
|
|
|
19
19
|
return catalog.resolve(root, agent)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Un cargo del sistema vive dentro del paquete: escribir ahí perdería el informe en el próximo
|
|
23
|
+
// `npm ci`, y además duplicaría en cada empresa una investigación sobre la profesión que se hace
|
|
24
|
+
// mejor una sola vez. Lo que sí es de esta empresa es su contexto, y ese tiene otro lugar.
|
|
25
|
+
function assertWritable(root, agent) {
|
|
26
|
+
const found = catalog.find(root, agent)
|
|
27
|
+
// Lo que decide no es si el cargo es del sistema, sino si vive dentro de este repositorio. En el
|
|
28
|
+
// toolkit los cargos del sistema son propios y se aprenden acá; en una instancia vienen del
|
|
29
|
+
// paquete, y escribir ahí se pierde en el próximo `npm ci` o en el próximo upgrade.
|
|
30
|
+
const own = path.resolve(catalog.projectCatalog(root))
|
|
31
|
+
if (path.resolve(found.dir).startsWith(`${own}${path.sep}`)) return found.dir
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${agent} es un cargo que trae Cauce y su aprendizaje se hace en el toolkit, no acá.\n` +
|
|
34
|
+
` Lo que este cargo debe saber de esta empresa va en organization/roles/${agent}.md.\n` +
|
|
35
|
+
` Para tener una versión propia del cargo, escribila en agents/roles/${agent}/.`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
function isoDate(now = new Date()) { return now.toISOString().slice(0, 10) }
|
|
23
40
|
function month(now = new Date()) { return now.toISOString().slice(0, 7) }
|
|
24
41
|
|
|
25
42
|
function prepareReport(root, agent, now = new Date()) {
|
|
26
|
-
const reports = path.join(
|
|
43
|
+
const reports = path.join(assertWritable(root, agent), 'learning', 'reports')
|
|
27
44
|
const file = path.join(reports, `${isoDate(now)}.md`)
|
|
28
45
|
fs.mkdirSync(reports, { recursive: true })
|
|
29
46
|
if (fs.existsSync(file)) return { file, created: false }
|
|
@@ -51,7 +68,7 @@ status: draft
|
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
function prepareProposal(root, agent, now = new Date()) {
|
|
54
|
-
const target =
|
|
71
|
+
const target = assertWritable(root, agent)
|
|
55
72
|
const proposalDir = path.join(target, 'learning', 'proposals')
|
|
56
73
|
const file = path.join(proposalDir, `${month(now)}.md`)
|
|
57
74
|
fs.mkdirSync(proposalDir, { recursive: true })
|