@ingeniomaps/cauce 0.99.0 → 0.99.2
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 +28 -0
- package/agents/roles/system/ai-product-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/analytics-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/backend-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/cloud-architect/learning/HISTORY.md +1 -0
- package/agents/roles/system/customer-success-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/data-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/data-governance-steward/learning/HISTORY.md +1 -0
- package/agents/roles/system/devops-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/engineering-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/engineering-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/engineering-manager/references/operating-model.md +1 -1
- package/agents/roles/system/machine-learning-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/mlops-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/mobile-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/product-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/site-reliability-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/software-architect/learning/HISTORY.md +1 -0
- package/agents/roles/system/ui-designer/learning/HISTORY.md +1 -0
- package/automatization/workflows/agent-promote.js +3 -1
- package/engine/agents/learning-seal.js +20 -1
- package/engine/cli/args.js +1 -1
- package/engine/cli/catalog.js +8 -2
- package/engine/cli/contract.js +9 -5
- package/engine/cli/ops.js +1 -1
- package/engine/cli/validate.js +1 -1
- package/engine/core/changelog.js +60 -1
- package/engine/core/repos.js +16 -12
- package/engine/hooks/approval.js +41 -7
- package/engine/hooks/files.js +3 -3
- package/engine/hooks/secrets-shell.js +2 -1
- package/engine/hooks/self-approval.js +39 -22
- package/package.json +1 -1
- package/template/AGENTS.md +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,34 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.99.2] - 2026-09-28
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Cada límite de `### Límites` llega una sola vez.** Si la primera viñeta empezaba con «El runner»,
|
|
22
|
+
«Debe» o «Nunca», la lista entera llegaba otra vez a cada agente, pegada en un límite más. Si reordenaste
|
|
23
|
+
tus viñetas para esquivarlo, ya podés dejarlas como quieras.
|
|
24
|
+
- **`check` deja de avisar que una fila resuelta de `HUMAN_ACTIONS.md` no está commiteada cuando sí lo
|
|
25
|
+
está.** Pasaba con filas largas: la búsqueda de git que usaba no encuentra algunas de más de 256 bytes.
|
|
26
|
+
Ahora se compara contra el archivo commiteado, y una fila que pasó a resuelta sólo en tu árbol se sigue
|
|
27
|
+
avisando.
|
|
28
|
+
|
|
29
|
+
## [0.99.1] - 2026-09-25
|
|
30
|
+
|
|
31
|
+
### Cambiado
|
|
32
|
+
|
|
33
|
+
- **Una lectura de credencial que necesitás siempre se aprueba una vez.** Cuando el agente se frena leyendo
|
|
34
|
+
una credencial, te pregunta además si querés dejarla aprobada para siempre. Si esa es tu intención —con
|
|
35
|
+
tus palabras, sin frase fija—, él mismo escribe la ruta en `planning/.ops-approval` con quién lo aprobó y
|
|
36
|
+
para qué, y ninguna sesión vuelve a preguntarlo. Tu confirmación le deja escribir esa línea y ninguna otra.
|
|
37
|
+
- **Confirmar el bloqueo de `.ops-approval` ahora aprueba.** Si el agente intenta escribir la aprobación sin
|
|
38
|
+
que se la hayas pedido, el bloqueo te muestra qué iba a agregar y tu «sí» lo deja pasar. Antes el bloqueo
|
|
39
|
+
ofrecía confirmar y la confirmación no hacía nada: sólo pasaba pidiéndolo con el nombre del archivo.
|
|
40
|
+
- **El bloqueo dice cuánto dura una línea pegada**: hasta que alguien la borre. Decía que dejaba de valer
|
|
41
|
+
«en cuanto cambie», y una lectura siempre frena la misma ruta, así que nunca cambiaba.
|
|
42
|
+
- **`check` nombra aparte las credenciales aprobadas** —«N credencial(es) aprobadas hasta que se borre su
|
|
43
|
+
línea»—, en vez de contarlas como una aprobación olvidada. Sigue mostrándolas en cada corrida.
|
|
44
|
+
|
|
17
45
|
## [0.99.0] - 2026-09-24
|
|
18
46
|
|
|
19
47
|
### Agregado
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -5,3 +5,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
7
|
| 2026-08-30 | `learning/proposals/2026-08.md` | Aprobada | @ingeniomaps (Manuel Pinzon) | Nada: el período se revisó y se decidió no cambiar ningún contrato. |
|
|
8
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -5,3 +5,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
7
|
| 2026-08-30 | `agents/roles/system/cloud-architect/learning/proposals/2026-08.md` | Aprobada | @ingeniomaps | `agents/roles/system/cloud-architect/SKILL.md` |
|
|
8
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -5,3 +5,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
7
|
| 2026-08-30 | `learning/proposals/2026-08.md` | Aprobada | Manuel Pinzon | `SKILL.md` (párrafo nuevo al final de «Construir contexto»); `learning/proposals/2026-08.md`. Dos desviaciones, escritas al final de «Aprobación humana» de la propuesta: (1) los ejemplos del párrafo se adaptaron al vocabulario del cargo —la propuesta autoriza adaptar la redacción y la licencia se usó sólo en esa lista: donde decía «Delighted, una herramienta de analítica», el `SKILL.md` dice «la encuesta de NPS o CSAT, el producto de analítica», porque Delighted es el proveedor del caso `07-nps-que-subio` y nombrarlo metía un caso de evaluación dentro del contrato del cargo—; el resto entró literal, incluida «abstenerse cubre lo que no se puede consultar, no lo que cuesta abrir una página», y en la ubicación que la propuesta fija; (2) no se re-corrió `07-nps-que-subio`, que la sección «Evaluación» pide como confirmación: esta aplicación se limitó a «Cambio propuesto», y hasta que exista ese veredicto el efecto del párrafo en este cargo no está medido. Sin caso adversarial nuevo: la propuesta acota el cambio al párrafo y nada más. |
|
|
8
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -17,7 +17,7 @@ sources:
|
|
|
17
17
|
tier: profession
|
|
18
18
|
topics: [developer-productivity, wellbeing, flow, collaboration]
|
|
19
19
|
- name: Google re Work team effectiveness
|
|
20
|
-
url: https://rework.withgoogle.com/intl/en/guides/
|
|
20
|
+
url: https://rework.withgoogle.com/intl/en/guides/understand-team-effectiveness
|
|
21
21
|
tier: profession
|
|
22
22
|
topics: [psychological-safety, effectiveness]
|
|
23
23
|
- name: ACM Code of Ethics
|
|
@@ -54,7 +54,7 @@ Combinar satisfacción/bienestar, performance/outcomes, actividad contextual, co
|
|
|
54
54
|
|
|
55
55
|
- [DORA Capabilities](https://dora.dev/capabilities/): entrega sostenible, feedback, cultura y desempeño organizacional.
|
|
56
56
|
- **SPACE Framework**, ACM Queue 19(1) (sin enlace: `queue.acm.org` bloquea): productividad multidimensional, no reducible a actividad.
|
|
57
|
-
- [Google re:Work — Understand team effectiveness](https://rework.withgoogle.com/intl/en/guides/
|
|
57
|
+
- [Google re:Work — Understand team effectiveness](https://rework.withgoogle.com/intl/en/guides/understand-team-effectiveness): seguridad psicológica, confiabilidad, estructura, significado e impacto.
|
|
58
58
|
- [ACM Code of Ethics](https://www.acm.org/binaries/content/assets/about/acm-code-of-ethics-booklet.pdf): bienestar, justicia, privacidad, competencia y liderazgo responsable.
|
|
59
59
|
|
|
60
60
|
Verificar políticas laborales, jurisdicción y contexto real de cada empresa.
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -4,3 +4,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
4
4
|
|
|
5
5
|
| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
|
|
6
6
|
|---|---|---|---|---|
|
|
7
|
+
| 2026-09-01 | `2026-09.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -6,3 +6,4 @@ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o
|
|
|
6
6
|
|---|---|---|---|---|
|
|
7
7
|
| 2026-08-30 | `agents/roles/system/ui-designer/learning/proposals/2026-08.md` | Aprobada | Manuel Pinzon | `agents/roles/system/ui-designer/SKILL.md` |
|
|
8
8
|
| 2026-08-30 | `agents/roles/system/ui-designer/learning/proposals/2026-09.md` | Aprobada | @ingeniomaps (Manuel Pinzon) | `agents/roles/system/ui-designer/SKILL.md` |
|
|
9
|
+
| 2026-09-01 | `2026-09-r2.md` | archivada | malpisa1@gmail.com | Se archivó sin decidir: el documento quedó con el molde. |
|
|
@@ -184,9 +184,11 @@ await agent(
|
|
|
184
184
|
// Sellar es lo último: hasta que el cambio no está aplicado y registrado, la propuesta sigue
|
|
185
185
|
// pendiente. Lo hace el motor y no vos, a mano, porque marcar el estado editando frontmatter es
|
|
186
186
|
// exactamente el paso que se hace mal en silencio.
|
|
187
|
+
// En el toolkit, sellar además anota el cambio en el CHANGELOG para que salga en la versión siguiente.
|
|
188
|
+
// Un mes sin cambios no le cambia nada a quien actualiza, y `--unchanged` lo deja fuera.
|
|
187
189
|
await agent(
|
|
188
190
|
`From ${ROOT}, run "node ${signature.cli || 'tools/ops.js'} ` +
|
|
189
|
-
`learn ${AGENT} --applied --period ${PERIOD}" and report only ` +
|
|
191
|
+
`learn ${AGENT} --applied --period ${PERIOD}${NOTHING ? ' --unchanged' : ''}" and report only ` +
|
|
190
192
|
`what it printed. Change nothing else.`,
|
|
191
193
|
{ label: 'sella' },
|
|
192
194
|
)
|
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
isoDate, proposalFiles, proposalState, assertWritable, lastOfPeriod, undecided, SIGNED,
|
|
15
15
|
} = require('./learning-files')
|
|
16
16
|
const { section } = require('../planning/parser')
|
|
17
|
+
const ownership = require('../core/ownership')
|
|
18
|
+
const changelog = require('../core/changelog')
|
|
17
19
|
// La misma identidad con la que se reclama una tarea: quién es la persona, no qué runner corre.
|
|
18
20
|
const { owner } = require('../planning/claims')
|
|
19
21
|
|
|
@@ -28,7 +30,7 @@ const UNSEALED = /^-[ \t]*Estado:[ \t]*(?:pendiente|aprobada)[ \t]*$/mi
|
|
|
28
30
|
// aprobada con responsable — y «aprobada y aplicada» cumple eso—, así que volver a correrlo sobre una
|
|
29
31
|
// propuesta ya aplicada la aplicaba de nuevo. Como los cambios son aditivos por diseño, el resultado
|
|
30
32
|
// no es un error visible sino un contrato con cada viñeta y cada fuente duplicadas.
|
|
31
|
-
function seal(root, agent, period = '', kind = 'agent') {
|
|
33
|
+
function seal(root, agent, period = '', kind = 'agent', { note = true } = {}) {
|
|
32
34
|
const file = proposalFile(root, agent, period, kind)
|
|
33
35
|
const dir = path.dirname(file)
|
|
34
36
|
const text = fs.readFileSync(file, 'utf8')
|
|
@@ -66,9 +68,26 @@ function seal(root, agent, period = '', kind = 'agent') {
|
|
|
66
68
|
// así que el registro que la plantilla promete no existía nunca. Se escribe acá porque es determinista:
|
|
67
69
|
// la fecha, el documento, quién aprobó y qué dice que cambia, todo sale de lo que se acaba de sellar.
|
|
68
70
|
if (kind === 'flow') appendHistory(path.dirname(path.dirname(dir)), file, responsible.trim(), change)
|
|
71
|
+
// Sólo en el toolkit: ahí el cargo es del catálogo que se publica, y su cambio tiene que viajar en una
|
|
72
|
+
// versión. En una empresa el cargo es suyo y no hay CHANGELOG de Cauce que escribir. `note: false` es
|
|
73
|
+
// el mes revisado sin cambios, que se sella igual y no le cambia nada a quien actualiza.
|
|
74
|
+
if (kind === 'agent' && note && ownership.mode(root) === 'toolkit') {
|
|
75
|
+
const noted = changelog.noteAgentChange(root, {
|
|
76
|
+
agent, proposal: path.basename(file), summary: summarize(change), today: isoDate(new Date()),
|
|
77
|
+
})
|
|
78
|
+
return { file, already: false, noted }
|
|
79
|
+
}
|
|
69
80
|
return { file, already: false }
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
// La primera idea de «Cambio propuesto», en una línea: el documento entero está en el cargo, y el
|
|
84
|
+
// CHANGELOG lo lee quien actualiza antes de decidir, no quien aplica.
|
|
85
|
+
function summarize(change, limit = 240) {
|
|
86
|
+
const first = change.split(/\n\s*\n/)[0].replace(/^\s*(?:\d+\.|[-*])\s+/, '').replace(/\s+/g, ' ').trim()
|
|
87
|
+
if (first.length <= limit) return first.replace(/[.:;]$/, '')
|
|
88
|
+
return `${first.slice(0, first.lastIndexOf(' ', limit)).replace(/[.,:;]$/, '')}…`
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
// Archivar es el tercer destino de una propuesta, y hasta ahora no existía: aplicarla, dejarla
|
|
73
92
|
// esperando, o **mirarla y decidir que no cambia nada**. Ese tercero se venía haciendo mergeando el PR
|
|
74
93
|
// sin firmar, que deja el documento en `proposed` para siempre — indistinguible de una que espera
|
package/engine/cli/args.js
CHANGED
|
@@ -36,7 +36,7 @@ const FLAGS = {
|
|
|
36
36
|
integration: ['--fixture'],
|
|
37
37
|
secrets: [],
|
|
38
38
|
automation: ['--force'],
|
|
39
|
-
learn: ['--flow', '--proposal', '--applied', '--archived', '--period', '--reason'],
|
|
39
|
+
learn: ['--flow', '--proposal', '--applied', '--unchanged', '--archived', '--period', '--reason'],
|
|
40
40
|
evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
|
|
41
41
|
flow: ['--json'],
|
|
42
42
|
}
|
package/engine/cli/catalog.js
CHANGED
|
@@ -104,11 +104,17 @@ function learn(agent, cli) {
|
|
|
104
104
|
: `✓ ${relative} queda archivada: ${porque}`)
|
|
105
105
|
}
|
|
106
106
|
if (cli.has('--applied')) {
|
|
107
|
-
const result = L.seal(opsRoot(), agent, cli.value('--period'), kind)
|
|
107
|
+
const result = L.seal(opsRoot(), agent, cli.value('--period'), kind, { note: !cli.has('--unchanged') })
|
|
108
108
|
const relative = path.relative(opsRoot(), result.file)
|
|
109
|
-
|
|
109
|
+
console.log(result.already
|
|
110
110
|
? `= ${relative} ya estaba aplicada`
|
|
111
111
|
: `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
|
|
112
|
+
if (result.noted) {
|
|
113
|
+
console.log(result.noted.already
|
|
114
|
+
? `= CHANGELOG.md ya nombraba este cambio en ${result.noted.version}`
|
|
115
|
+
: `✓ CHANGELOG.md: el cambio sale en ${result.noted.version}`)
|
|
116
|
+
}
|
|
117
|
+
return
|
|
112
118
|
}
|
|
113
119
|
// Un recorrido no tiene informe de investigación: su propuesta se compone de los veredictos de sus
|
|
114
120
|
// propias corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre su
|
package/engine/cli/contract.js
CHANGED
|
@@ -119,17 +119,21 @@ function marked(raw) {
|
|
|
119
119
|
return { bullets, intros, own }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// La pasada de prosa descuenta las líneas que ya son viñetas declaradas. Para ella un bloque de viñetas
|
|
123
|
+
// seguidas es un solo párrafo, así que si la primera empezaba con «El runner» la lista entera entraba otra
|
|
124
|
+
// vez, pegada en un límite: cada uno llegaba dos veces al preámbulo (caso 203). Es el mismo descuento por
|
|
125
|
+
// línea que hace `warnings`, y por la misma razón.
|
|
124
126
|
function limits(text) {
|
|
127
|
+
const { bullets, own } = marked(text)
|
|
128
|
+
const taken = new Set(own)
|
|
125
129
|
const prose = text.split(/\n\s*\n/)
|
|
126
130
|
.map((block) => block.split('\n')
|
|
127
131
|
.map((line) => line.replace(/^[-*]\s+/, '').trim())
|
|
128
|
-
.filter((line) => line && !line.startsWith('#') && !line.startsWith('|'))
|
|
132
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('|') && !taken.has(line))
|
|
129
133
|
.join(' ')
|
|
130
134
|
.trim())
|
|
131
135
|
.filter((block) => ENUNCIA.test(block))
|
|
132
|
-
return [...
|
|
136
|
+
return [...bullets, ...prose]
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
// Lo que el proyecto escribió en su sección de excepciones y **no** llegó a `boundaries`. Existe porque
|
|
@@ -163,7 +167,7 @@ function warnings(root) {
|
|
|
163
167
|
))
|
|
164
168
|
// Se descuenta por línea y no por párrafo, que es donde estaba el defecto: `paragraphs` saca el `- ` y
|
|
165
169
|
// une las viñetas seguidas en un párrafo solo, así que una lista declarada no era igual a ninguna
|
|
166
|
-
//
|
|
170
|
+
// viñeta de `marked()` y se contaba entera como un límite perdido (caso 159).
|
|
167
171
|
const { own, intros } = marked(mine)
|
|
168
172
|
const suyo = new Set([...own, ...intros])
|
|
169
173
|
const outside = P.withoutComments(mine).split('\n')
|
package/engine/cli/ops.js
CHANGED
|
@@ -180,7 +180,7 @@ function usage() {
|
|
|
180
180
|
ops automation doctor <ops-root> claude|codex|gemini|antigravity
|
|
181
181
|
ops automation install <ops-root> claude|codex|gemini|antigravity
|
|
182
182
|
ops automation uninstall <ops-root> claude|codex|gemini|antigravity
|
|
183
|
-
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>]]
|
|
183
|
+
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>] [--unchanged]]
|
|
184
184
|
ops learn <agent|flow> [--flow] --archived [--period <AAAA-MM>] [--reason <motivo>]
|
|
185
185
|
ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
|
|
186
186
|
ops agents list [ops-root] [--own|--system] [--json]
|
package/engine/cli/validate.js
CHANGED
|
@@ -132,7 +132,7 @@ function check(dir, cli) {
|
|
|
132
132
|
warnings.push(...RC.warnings(RC.status({ ...recurring, done, today: TODAY() })))
|
|
133
133
|
warnings.push(...IB.warnings(root, done, config))
|
|
134
134
|
warnings.push(...AD.sealWarnings(root))
|
|
135
|
-
warnings.push(...R.unrecordedHumanActions(path.resolve(root, '..'), P.readHumanActions(root)))
|
|
135
|
+
warnings.push(...R.unrecordedHumanActions(path.resolve(root, '..'), P.readHumanActions(root), P.withoutComments))
|
|
136
136
|
warnings.push(...AP.warnings(path.resolve(root, '..')))
|
|
137
137
|
warnings.push(...TR.warnings(path.resolve(root, '..')))
|
|
138
138
|
warnings.push(...CT.warnings(path.resolve(root, '..')))
|
package/engine/core/changelog.js
CHANGED
|
@@ -47,4 +47,63 @@ function read(packageRoot) {
|
|
|
47
47
|
try { return fs.readFileSync(path.join(packageRoot, 'CHANGELOG.md'), 'utf8') } catch { return '' }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// La línea que `agent-promote` deja al aplicar un cargo, en la versión que todavía no salió. Sin ella el
|
|
51
|
+
// cambio quedaba en `main` sin versión que lo llevara: `release-pr.yml` sólo abre el PR cuando el
|
|
52
|
+
// CHANGELOG declara un número nuevo, y escribirlo era un paso que nadie tenía asignado.
|
|
53
|
+
//
|
|
54
|
+
// Abierta es la entrada más nueva si va por delante de `package.json`; si no, ya salió y se abre la
|
|
55
|
+
// minor siguiente. Minor porque cambia lo que recibe quien hace `upgrade`, que es la regla de este
|
|
56
|
+
// archivo. Varios cargos del mismo mes caen así en una sola versión.
|
|
57
|
+
function noteAgentChange(root, { agent, proposal, summary, today }) {
|
|
58
|
+
const file = path.join(root, 'CHANGELOG.md')
|
|
59
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n')
|
|
60
|
+
const released = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version
|
|
61
|
+
const first = lines.findIndex((line) => /^##\s*\[\d/.test(line))
|
|
62
|
+
if (first < 0) throw new Error('CHANGELOG.md no tiene ninguna entrada con versión.')
|
|
63
|
+
const bullet = wrap(`- **\`${agent}\`**: ${summary} (propuesta \`${proposal}\`).`)
|
|
64
|
+
const top = lines[first].match(HEADING)[1]
|
|
65
|
+
if (compare(top, released) <= 0) {
|
|
66
|
+
const [major, minor] = released.split('.').map(Number)
|
|
67
|
+
const version = `${major}.${minor + 1}.0`
|
|
68
|
+
lines.splice(first, 0, `## [${version}] - ${today}`, '', '### Cargos', '', ...bullet, '')
|
|
69
|
+
fs.writeFileSync(file, lines.join('\n'))
|
|
70
|
+
return { version, already: false }
|
|
71
|
+
}
|
|
72
|
+
let end = lines.findIndex((line, index) => index > first && HEADING.test(line))
|
|
73
|
+
if (end < 0) end = lines.length
|
|
74
|
+
// Una propuesta ya anotada no se anota dos veces: `seal` puede volver a correr sobre un cuerpo sin sellar.
|
|
75
|
+
// Con los espacios normalizados porque la viñeta se envuelve, y el corte puede caer entre las dos mitades.
|
|
76
|
+
const entry = lines.slice(first, end).join(' ').replace(/\s+/g, ' ')
|
|
77
|
+
if (entry.includes(`- **\`${agent}\`**`) && entry.includes(`(propuesta \`${proposal}\`)`)) {
|
|
78
|
+
return { version: top, already: true }
|
|
79
|
+
}
|
|
80
|
+
const section = lines.findIndex((line, index) => index > first && index < end && /^###\s+Cargos\s*$/.test(line))
|
|
81
|
+
let at = end
|
|
82
|
+
if (section >= 0) {
|
|
83
|
+
at = lines.findIndex((line, index) => index > section && index < end && /^#{2,3}\s/.test(line))
|
|
84
|
+
if (at < 0) at = end
|
|
85
|
+
}
|
|
86
|
+
while (at > first + 1 && lines[at - 1].trim() === '') at--
|
|
87
|
+
lines.splice(at, 0, ...(section >= 0 ? bullet : ['', '### Cargos', '', ...bullet]))
|
|
88
|
+
fs.writeFileSync(file, lines.join('\n'))
|
|
89
|
+
return { version: top, already: false }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Al ancho del resto del archivo, con la sangría de continuación que ya usan sus viñetas.
|
|
93
|
+
function wrap(text, width = 110) {
|
|
94
|
+
const out = []
|
|
95
|
+
let line = ''
|
|
96
|
+
for (const word of text.split(/\s+/)) {
|
|
97
|
+
const next = line ? `${line} ${word}` : word
|
|
98
|
+
if (next.length > width && line) {
|
|
99
|
+
out.push(line)
|
|
100
|
+
line = ` ${word}`
|
|
101
|
+
} else {
|
|
102
|
+
line = next
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (line) out.push(line)
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { between, compare, entries, read, noteAgentChange }
|
package/engine/core/repos.js
CHANGED
|
@@ -130,26 +130,30 @@ function coverageWarnings(opsRoot, done) {
|
|
|
130
130
|
// defecto se descubría en la fase 4 de un recorrido: 1,21 M de tokens en tres paradas, con la puerta en
|
|
131
131
|
// verde las tres veces (caso 121).
|
|
132
132
|
//
|
|
133
|
-
// Se pregunta con el
|
|
134
|
-
//
|
|
135
|
-
//
|
|
133
|
+
// Se pregunta si **esa** fila, con ese estado, está en el archivo de `HEAD`, línea por línea, y no por la
|
|
134
|
+
// palabra `resuelta`. Una que pasó a resuelta sólo en el árbol de trabajo no está ahí.
|
|
135
|
+
//
|
|
136
|
+
// No se pregunta con el pickaxe, que era la primera forma: `git log -S` busca con una tabla de saltos de
|
|
137
|
+
// un byte (`kwset.c`), que con una aguja de más de 256 bytes se desborda y salta por encima de una fila que
|
|
138
|
+
// sí está en el commit. Una fila resuelta de verdad es justo la larga, porque lleva la decisión adentro
|
|
139
|
+
// (caso 204).
|
|
140
|
+
//
|
|
141
|
+
// `clean` es la limpieza que ya sufrieron las filas: el parser las lee sin comentarios, así que el archivo
|
|
142
|
+
// commiteado tiene que pasar por la misma o una fila con un `<!-- -->` adentro no coincide con su línea.
|
|
136
143
|
//
|
|
137
144
|
// Sin repositorio, o con el archivo todavía sin commitear, no dice nada: no hay historia contra la cual
|
|
138
145
|
// preguntar y el aviso sería inventado. Degrada como el 086 con las migraciones — antes callar de más que
|
|
139
146
|
// avisar de más, porque un aviso que salta siempre se termina apagando.
|
|
140
|
-
function unrecordedHumanActions(opsRoot, rows) {
|
|
147
|
+
function unrecordedHumanActions(opsRoot, rows, clean) {
|
|
141
148
|
const file = path.join(opsRoot, 'planning', 'HUMAN_ACTIONS.md')
|
|
142
149
|
const top = git(path.dirname(file), 'rev-parse', '--show-toplevel')
|
|
143
150
|
if (top.status !== 0) return []
|
|
144
151
|
const repo = top.stdout.trim()
|
|
145
|
-
const relative = path.relative(repo, file)
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const found = git(repo, 'log', '--format=%h', `-S${row.raw}`, '--', relative)
|
|
151
|
-
return found.status === 0 && !found.stdout.trim()
|
|
152
|
-
})
|
|
152
|
+
const relative = path.relative(repo, file).split(path.sep).join('/')
|
|
153
|
+
const committed = git(repo, 'show', `HEAD:${relative}`)
|
|
154
|
+
if (committed.status !== 0) return []
|
|
155
|
+
const lines = new Set(clean(committed.stdout).split('\n'))
|
|
156
|
+
return rows.filter((row) => row.resolved && !lines.has(row.raw))
|
|
153
157
|
.map((row) => `HUMAN_ACTIONS.md: ${row.task} figura resuelta y ningún commit la registró`)
|
|
154
158
|
}
|
|
155
159
|
|
package/engine/hooks/approval.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// **Se coteja, no se consume.** Borrar el archivo al leerlo daría el mismo alcance y traería dos cosas
|
|
16
16
|
// que no queremos: hoy ningún guard escribe en el repositorio, y `governance` corre antes que `verify`,
|
|
17
17
|
// así que un commit frenado por otra razón se habría llevado puesta la aprobación y habría que
|
|
18
|
-
// rehacerla. Cotejando, la aprobación vale para
|
|
19
|
-
//
|
|
18
|
+
// rehacerla. Cotejando, la aprobación vale para las rutas que nombra y para ninguna otra: lo que se sume
|
|
19
|
+
// al índice vuelve a frenar, que es «por operación» sin fecha ni contador.
|
|
20
20
|
//
|
|
21
21
|
// Queda a la vista porque `check` avisa mientras exista. Sin eso, un archivo olvidado sigue autorizando
|
|
22
22
|
// esas mismas rutas la próxima vez que alguien las stagee, que es la puerta abierta que esto evitaba.
|
|
@@ -104,7 +104,7 @@ function REFUSED(items, input = {}) {
|
|
|
104
104
|
// `pasteable` es lo que se puede aprobar por archivo, y por defecto es todo: un guard lo angosta cuando lo
|
|
105
105
|
// que tiene a mano no sirve para pegar —por qué, en `secrets-shell.js` (caso 118)—. Lo frenado se anota
|
|
106
106
|
// igual, así que el «dale» sigue cubriendo todo.
|
|
107
|
-
function HOW(variable, lines, input, pasteable = lines) {
|
|
107
|
+
function HOW(variable, lines, input, pasteable = lines, { durable = false } = {}) {
|
|
108
108
|
const held = CHAT.hold(input, lines)
|
|
109
109
|
const dropped = held ? held.dropped : []
|
|
110
110
|
const chat = held && dropped.length < lines.length
|
|
@@ -132,6 +132,7 @@ function HOW(variable, lines, input, pasteable = lines) {
|
|
|
132
132
|
+ 'en los mensajes siguientes hasta que ella lo niegue. '
|
|
133
133
|
: ''
|
|
134
134
|
const refused = dropped.length ? REFUSED(dropped, input) : ''
|
|
135
|
+
const keep = chat && durable ? KEEP(input, pasteable.filter((one) => !dropped.includes(one))) : ''
|
|
135
136
|
// Sin chat la salida es la misma, y también se dice como cosa de ella: el imperativo que el párrafo de
|
|
136
137
|
// arriba sacó de la rama con chat seguía acá, y es lo único que ve quien no tiene a nadie en el chat
|
|
137
138
|
// (caso 194). A quien corre el guard le toca otra cosa, y se le dice cuál. Vale también cuando hay persona
|
|
@@ -141,7 +142,7 @@ function HOW(variable, lines, input, pasteable = lines) {
|
|
|
141
142
|
+ ' tal cual en'
|
|
142
143
|
+ ` ${where(input)} estas líneas:\n`
|
|
143
144
|
+ pasteable.map((line) => ` ${line}\n`).join('')
|
|
144
|
-
+ '
|
|
145
|
+
+ 'Cada línea vale hasta que alguien la borre, y lo que no esté ahí vuelve a frenar. '
|
|
145
146
|
+ (chat ? '' : 'Vos no lo escribas —un guard lo frena—: decí qué se frenó y dónde, y reintentá cuando '
|
|
146
147
|
+ 'esté; si sos un subagente, devolvele el bloqueo a quien te lanzó. ')
|
|
147
148
|
: ''
|
|
@@ -157,7 +158,28 @@ function HOW(variable, lines, input, pasteable = lines) {
|
|
|
157
158
|
? `La variable ${variable}=1 sigue existiendo y apaga el guard para toda la sesión, que es por lo que no `
|
|
158
159
|
+ 'es la vía recomendada.'
|
|
159
160
|
: ''
|
|
160
|
-
return ask + refused + paste + unresolved + off
|
|
161
|
+
return ask + keep + refused + paste + unresolved + off
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// La lectura que un proyecto necesita siempre —el token con que su regla manda autenticar— se preguntaba en
|
|
165
|
+
// cada sesión, porque el «dale» dura la sesión y la línea del archivo la tenía que pegar la persona a mano:
|
|
166
|
+
// «pon esa línea» o «acepto que leas» no le alcanzaban al agente para escribírsela (caso 202).
|
|
167
|
+
//
|
|
168
|
+
// La pregunta va entera al agente, y no se le pide a la persona ninguna frase: cuál de las dos cosas quiso la
|
|
169
|
+
// juzga él, igual que el «dale» (caso 184). Lo que hace el guard es dejar anotados, además de lo frenado, el
|
|
170
|
+
// archivo de aprobación, así que la misma confirmación que habilita leer habilita escribir **esas líneas y
|
|
171
|
+
// ninguna otra** —cuáles, lo compara `self-approval`—. Sólo lo ofrece un guard de lectura: una lectura
|
|
172
|
+
// frena siempre la misma ruta, y ahí una línea permanente es lo que la persona está pidiendo; para un
|
|
173
|
+
// commit, lo frenado cambia con el índice y dejarlo escrito no le ahorra nada a nadie.
|
|
174
|
+
function KEEP(input, lines) {
|
|
175
|
+
const root = opsRoot(input)
|
|
176
|
+
if (!root || !lines.length) return ''
|
|
177
|
+
const held = CHAT.hold(input, [path.join(root, 'planning', APPROVAL)])
|
|
178
|
+
if (!held || held.dropped.length) return ''
|
|
179
|
+
return 'Preguntale también si quiere dejarlo aprobado para siempre, para no volver a preguntarlo en otra '
|
|
180
|
+
+ `sesión. Si esa es su intención, agregá a ${where(input)} las líneas de abajo, con un comentario arriba que `
|
|
181
|
+
+ 'diga quién lo aprobó, cuándo y para qué: la misma confirmación te deja escribirlas. Si sólo quiso que '
|
|
182
|
+
+ 'leas ahora, no las escribas y vale para esta sesión. '
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
// Las dos exenciones que sobreviven a un bloqueo, para que `check` las muestre juntas: la lista que una
|
|
@@ -168,11 +190,23 @@ function HOW(variable, lines, input, pasteable = lines) {
|
|
|
168
190
|
// repetir la autorización en cada mensaje —y eso está bien—, pero quedó del lado que nadie audita: vive en
|
|
169
191
|
// el temporal del sistema, mientras que por una sola línea del archivo `check` sí avisaba. Una exención que
|
|
170
192
|
// no se ve es un límite que ya no existe (caso 117).
|
|
193
|
+
//
|
|
194
|
+
// Una credencial aprobada se nombra aparte, y no como algo «sin borrar». Está ahí a propósito —la dejó
|
|
195
|
+
// escrita la persona para no volver a autorizar la misma lectura en cada sesión (caso 202)—, y contada
|
|
196
|
+
// junto a lo que quedó de un commit se leía como un olvido que había que limpiar. Sigue saliendo en cada
|
|
197
|
+
// corrida, igual que `writableOutsideRoots`: una exención deliberada también se ve.
|
|
171
198
|
function warnings(root) {
|
|
172
199
|
const out = []
|
|
200
|
+
const { credential } = require('./files')
|
|
173
201
|
const approved = read(root)
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
const secret = approved.filter((line) => credential({ cwd: root }, line))
|
|
203
|
+
const rest = approved.filter((line) => !secret.includes(line))
|
|
204
|
+
if (secret.length) {
|
|
205
|
+
out.push(`planning/${APPROVAL}: ${secret.length} credencial(es) aprobadas hasta que se borre su línea: `
|
|
206
|
+
+ secret.join(', '))
|
|
207
|
+
}
|
|
208
|
+
if (rest.length) {
|
|
209
|
+
out.push(`planning/${APPROVAL}: ${rest.length} ruta(s) aprobadas y sin borrar; `
|
|
176
210
|
+ 'el archivo sigue autorizándolas')
|
|
177
211
|
}
|
|
178
212
|
const granted = CHAT.grantedIn(root)
|
package/engine/hooks/files.js
CHANGED
|
@@ -19,8 +19,8 @@ const { hasTasks } = require('../planning/state')
|
|
|
19
19
|
const { TEMPLATE_PREFIXES } = require('../core/ownership')
|
|
20
20
|
|
|
21
21
|
// Si la ruta que este guard está por bloquear está aprobada, no hay nada que decir. Es la salida
|
|
22
|
-
// angosta: vale para esa ruta y
|
|
23
|
-
//
|
|
22
|
+
// angosta: vale para esa ruta y para ninguna otra, a diferencia de la variable, que apaga el guard hasta
|
|
23
|
+
// que cierre la sesión.
|
|
24
24
|
const approved = (input, file) => !AP.pending(opsRoot(input), [file], input).length
|
|
25
25
|
|
|
26
26
|
// Qué archivo es una credencial, para los dos guards que la cuidan: `secrets`, que frena escribirla, y
|
|
@@ -78,7 +78,7 @@ function secretsRead(input) {
|
|
|
78
78
|
for (const file of [...filesOf(input), ...patterns]) {
|
|
79
79
|
if (!patternNames(file).some((name) => credential(input, name)) || approved(input, file)) continue
|
|
80
80
|
block(`${file} es una credencial: leerla la deja en el contexto de la sesión. Si hace falta un valor, `
|
|
81
|
-
+ `pedíselo a una persona.\n${AP.HOW('OPS_SECRETS_READ_OVERRIDE', [file], input)}`)
|
|
81
|
+
+ `pedíselo a una persona.\n${AP.HOW('OPS_SECRETS_READ_OVERRIDE', [file], input, [file], { durable: true })}`)
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -67,7 +67,8 @@ function secretsShell(input) {
|
|
|
67
67
|
if (!left.length) return
|
|
68
68
|
block(`el comando lee ${left.join(', ')}, que es una credencial: leerla la deja en el contexto de la sesión. `
|
|
69
69
|
+ 'Si hace falta un valor, pedíselo a una persona.\n'
|
|
70
|
-
+ AP.HOW('OPS_SECRETS_READ_OVERRIDE', left, input, left.filter((one) => !UNRESOLVED.test(one))
|
|
70
|
+
+ AP.HOW('OPS_SECRETS_READ_OVERRIDE', left, input, left.filter((one) => !UNRESOLVED.test(one)),
|
|
71
|
+
{ durable: true }))
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
module.exports = { secretsShell }
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Lo preguntan los dos guards de límites —el que mira un `Write` y el que mira el destino de un comando—,
|
|
8
8
|
// que ya son los que deciden dónde puede caer una escritura. La persona edita el archivo a mano, que ningún
|
|
9
|
-
// hook ve, o se lo pide al agente
|
|
10
|
-
// herramienta.
|
|
9
|
+
// hook ve, o se lo pide al agente en el chat: nombrándolo, o confirmando el bloqueo que le mostró qué iba a
|
|
10
|
+
// escribir. El registro del chat no lo escribe nunca una herramienta.
|
|
11
11
|
//
|
|
12
12
|
// **Nombrar el archivo autoriza el archivo, no lo que se escribe adentro**, y hasta 0.83.0 eso era todo lo
|
|
13
13
|
// que se comprobaba: la persona pedía agregar una ruta inocua, el agente escribía `push origin main`, y esa
|
|
@@ -34,6 +34,12 @@ const UNASKED = (file, missing) => `${file} es la aprobación de una persona y e
|
|
|
34
34
|
+ 'Escribí sólo lo que ella nombró en su mensaje. Si hacen falta las otras, decile cuáles y por qué, y '
|
|
35
35
|
+ 'pedile que lo confirme con sus palabras: si lo que contesta es un sí, reintentá la misma escritura y pasa.'
|
|
36
36
|
|
|
37
|
+
// Lo mismo que `UNASKED`, para cuando además el archivo no se nombró: ninguna de las líneas está pedida.
|
|
38
|
+
const UNNAMED = (file, lines) => `${file} es la aprobación de una persona, y escribírsela es aprobarse solo. `
|
|
39
|
+
+ 'Esta escritura agrega:\n' + lines.map((line) => ` ${line}\n`).join('')
|
|
40
|
+
+ 'Decile qué querés dejar aprobado y por qué, y pedile que lo confirme con sus palabras: si lo que contesta '
|
|
41
|
+
+ 'es un sí, reintentá la misma escritura y pasa. Si no, que lo edite ella.'
|
|
42
|
+
|
|
37
43
|
// **Por shell se frena toda escritura.** Es la misma decisión que toma `ops-config`, y por qué un comando
|
|
38
44
|
// no se puede comparar está escrito allá. Lo que la trae hasta acá es que el contenido es lo único que
|
|
39
45
|
// separa la línea que la persona pidió de la que el agente se escribe solo.
|
|
@@ -49,15 +55,31 @@ function approvalRoot(input, file) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// Lo que las dos vías deciden igual: el registro del chat no se escribe nunca, y la aprobación sólo si la
|
|
52
|
-
// persona la nombró en su mensaje.
|
|
58
|
+
// persona la nombró en su mensaje o confirmó el bloqueo que la frenaba.
|
|
53
59
|
//
|
|
54
60
|
// Se pregunta sin conceder: una concesión que sobreviviera al mensaje convertiría «agregá src/x.js a
|
|
55
61
|
// .ops-approval» en permiso para escribirle después cualquier otra línea, que es aprobarse solo por la
|
|
56
62
|
// puerta de al lado.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
const named = (input, file) => CHAT.authorized(input, [file]).length > 0
|
|
64
|
+
const chatRecord = (file) => file === CHAT.DIR || file.startsWith(`${CHAT.DIR}${path.sep}`)
|
|
65
|
+
|
|
66
|
+
// Lo que esta escritura agrega al archivo. **Lo que ya estaba en disco no se vuelve a nombrar**: esta
|
|
67
|
+
// escritura no lo agrega, y exigirlo obligaría a repetir el archivo entero para sumar una línea. Quitar
|
|
68
|
+
// tampoco se pregunta: una aprobación más corta autoriza menos.
|
|
69
|
+
function added(input, root) {
|
|
70
|
+
const filed = new Set(AP.read(root))
|
|
71
|
+
return AP.lines(contentOf(input)).filter((line) => !filed.has(line))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// El archivo sin nombrar frenaba sin anotar nada, así que la salida que el propio mensaje ofrecía
|
|
75
|
+
// —confirmar el bloqueo— no aprobaba nada: el «dale» siguiente volvía a frenar igual, y la persona sólo
|
|
76
|
+
// podía destrabarlo con la frase exacta que nombra el archivo con un verbo (caso 202). Se anota el archivo
|
|
77
|
+
// con las líneas que trae, que es lo que la confirmación va a aprobar y lo que el mensaje le muestra.
|
|
78
|
+
function unnamed(input, root, file) {
|
|
79
|
+
const lines = added(input, root)
|
|
80
|
+
const held = lines.length ? CHAT.hold(input, [file, ...lines]) : false
|
|
81
|
+
if (!held || held.dropped.length) return SELF(file)
|
|
82
|
+
return UNNAMED(file, lines)
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
// Una línea de push no se pregunta como una ruta: `mentions` compara también el basename, así que para
|
|
@@ -67,10 +89,6 @@ const isPush = (line) => /^push\s+\S+\s+\S+$/.test(line)
|
|
|
67
89
|
|
|
68
90
|
// Por qué no se puede escribir este contenido, o vacío.
|
|
69
91
|
//
|
|
70
|
-
// **Lo que ya estaba en disco no se vuelve a nombrar**: esta escritura no lo agrega, y exigirlo obligaría a
|
|
71
|
-
// repetir el archivo entero para sumar una línea. Quitar tampoco se pregunta: una aprobación más corta
|
|
72
|
-
// autoriza menos.
|
|
73
|
-
//
|
|
74
92
|
// **Un fragmento sí se juzga, al revés que en `ops.config.json`.** Allá el archivo entrante se compara
|
|
75
93
|
// entero porque una llave cambia de sentido según lo que la rodea, y un `Edit` manda un pedazo; acá cada
|
|
76
94
|
// línea vale por sí sola, así que el pedazo que llega es exactamente lo que se agrega. Lo que no se puede
|
|
@@ -80,12 +98,11 @@ const isPush = (line) => /^push\s+\S+\s+\S+$/.test(line)
|
|
|
80
98
|
// Y se pregunta sin heredar lo que la sesión concedió antes: una línea acá la leen todos los guards y llega
|
|
81
99
|
// hasta la rama viva, así que tiene que ser la que la persona dijo en el mensaje en curso.
|
|
82
100
|
function unasked(input, root, file) {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const paths = CHAT.authorized(input, added.filter((line) => !isPush(line)), { inherit: false })
|
|
101
|
+
const lines = added(input, root)
|
|
102
|
+
const pushes = CHAT.authorized(input, lines.filter(isPush), { asked: CHAT.ordersPush, inherit: false })
|
|
103
|
+
const paths = CHAT.authorized(input, lines.filter((line) => !isPush(line)), { inherit: false })
|
|
87
104
|
const cleared = new Set([...pushes, ...paths].map((one) => one.item))
|
|
88
|
-
const missing =
|
|
105
|
+
const missing = lines.filter((line) => !cleared.has(line))
|
|
89
106
|
if (!missing.length) return ''
|
|
90
107
|
// El archivo se anota junto con las líneas: sin él, el «dale» aprobaría las líneas y el guard volvería a
|
|
91
108
|
// frenar por el archivo, que en ese mensaje ya nadie nombra.
|
|
@@ -95,17 +112,17 @@ function unasked(input, root, file) {
|
|
|
95
112
|
|
|
96
113
|
// Por qué el agente no puede escribir en `file` con la herramienta de edición, o vacío.
|
|
97
114
|
function selfApproval(input, file) {
|
|
98
|
-
|
|
99
|
-
if (stop) return stop
|
|
115
|
+
if (chatRecord(file)) return CHAT_RECORD(file)
|
|
100
116
|
const root = approvalRoot(input, file)
|
|
101
|
-
|
|
117
|
+
if (!root) return ''
|
|
118
|
+
return named(input, file) ? unasked(input, root, file) : unnamed(input, root, file)
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
// Lo mismo para el destino de un comando, o vacío.
|
|
105
122
|
function selfApprovalShell(input, file) {
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
return
|
|
123
|
+
if (chatRecord(file)) return CHAT_RECORD(file)
|
|
124
|
+
if (!approvalRoot(input, file)) return ''
|
|
125
|
+
return named(input, file) ? BY_COMMAND(file) : SELF(file)
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
module.exports = { selfApproval, selfApprovalShell }
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -122,7 +122,14 @@ para el agente con el que hablás.
|
|
|
122
122
|
autorizás, una por línea, con `#` para lo que no sea una ruta. En sidecar es el `planning/` de la
|
|
123
123
|
instancia y no una carpeta al lado de tus proyectos; el bloqueo dice la ruta exacta. Es un solo archivo
|
|
124
124
|
para todos los guards, porque lo que escribís son rutas y quién las mira lo decide qué guard esté
|
|
125
|
-
juzgando esa ruta. Lo escribís vos
|
|
125
|
+
juzgando esa ruta. Lo escribís vos, o el agente con tu confirmación: si intenta escribírselo solo, un
|
|
126
|
+
guard lo frena y te muestra qué iba a agregar.
|
|
127
|
+
|
|
128
|
+
**Una lectura que necesitás siempre se aprueba una vez.** El «dale» dura la sesión. Cuando el agente se
|
|
129
|
+
frena leyendo una credencial —el token con el que una regla tuya manda autenticar—, te pregunta además si
|
|
130
|
+
querés dejarla aprobada para siempre; si esa es tu intención, escribe la ruta en ese archivo, con quién lo
|
|
131
|
+
aprobó y para qué, y ninguna sesión vuelve a preguntarlo. `check` la sigue nombrando mientras exista, como
|
|
132
|
+
toda exención; se revoca borrando la línea.
|
|
126
133
|
|
|
127
134
|
| lo que te frena | qué ruta aprobás |
|
|
128
135
|
|---|---|
|