openprompt-lang 1.2.7 → 1.3.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.
@@ -1,8 +1,11 @@
1
1
  // @kind(module)
2
2
  // @contract(in: gateName:string, context:object -> out: {passed:boolean, reason:string} @error: WorkflowError)
3
- // @limit(lines: 120)
3
+ // @limit(lines: 340)
4
4
  // @pattern(provider)
5
5
 
6
+ import { join } from "path"
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs"
8
+
6
9
  /**
7
10
  * Módulo de gates de workflow.
8
11
  *
@@ -75,6 +78,182 @@ const GATES = Object.freeze({
75
78
  message: "El proyecto debe existir en la base de datos (opl project init o wizard)",
76
79
  description: "El proyecto debe estar registrado en SQLite",
77
80
  },
81
+
82
+ // ─── Nuevos gates v2 (workflow inteligente) ─────────────────────────────────
83
+
84
+ docs_updated: {
85
+ requires: "docs_updated",
86
+ message: "Debes actualizar la documentación antes de cerrar. Ejecuta: opl doc flow/framework",
87
+ description: "La documentación debe estar actualizada antes de cerrar la sesión",
88
+ customCheck: (ctx) => {
89
+ // Verificar que existe documentación reciente
90
+ const docsDir = join(process.cwd(), "docs")
91
+ let hasRecentDocs = false
92
+ try {
93
+ if (existsSync(docsDir)) {
94
+ const files = readdirSync(docsDir).filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
95
+ const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
96
+ for (const f of files) {
97
+ const stat = statSync(join(docsDir, f))
98
+ if (stat.mtimeMs > fourHoursAgo) {
99
+ hasRecentDocs = true
100
+ break
101
+ }
102
+ }
103
+ }
104
+ } catch {
105
+ // Si no se puede verificar, pasar con advertencia
106
+ }
107
+
108
+ if (hasRecentDocs || ctx.flags?.docs_updated) {
109
+ return { passed: true, reason: "Documentación actualizada verificada" }
110
+ }
111
+
112
+ const sessionPath = join(process.cwd(), ".opencode", "work-context", "SESSION.json")
113
+ let hasSessionDocs = false
114
+ try {
115
+ if (existsSync(sessionPath)) {
116
+ const session = JSON.parse(readFileSync(sessionPath, "utf-8"))
117
+ hasSessionDocs = !!session.docGenerated
118
+ }
119
+ } catch {
120
+ // Ignorar
121
+ }
122
+
123
+ if (hasSessionDocs) {
124
+ return { passed: true, reason: "Documentación de sesión generada" }
125
+ }
126
+
127
+ return {
128
+ passed: false,
129
+ reason:
130
+ "No se detectaron documentos actualizados recientemente. Debes generar la documentación antes de cerrar.",
131
+ }
132
+ },
133
+ },
134
+
135
+ ticket_created: {
136
+ requires: "ticket_created",
137
+ message:
138
+ 'Debes crear un ticket antes de implementar. Ejecuta: npx openPrompt-Lang ticket create --title "..."',
139
+ description: "Todo cambio debe tener un ticket asociado antes de implementar",
140
+ customCheck: (ctx) => {
141
+ const bugsDir = join(process.cwd(), ".opencode", "bugs")
142
+ let hasOpenTicket = false
143
+ let ticketCount = 0
144
+ try {
145
+ if (existsSync(bugsDir)) {
146
+ const files = readdirSync(bugsDir).filter((f) => f.endsWith(".md"))
147
+ ticketCount = files.length
148
+ for (const f of files) {
149
+ const content = readFileSync(join(bugsDir, f), "utf-8")
150
+ const statusMatch = content.match(/\*\*Estado\*\*\s+\|\s+(\w+)/)
151
+ if (statusMatch && ["open", "wip"].includes(statusMatch[1])) {
152
+ hasOpenTicket = true
153
+ break
154
+ }
155
+ }
156
+ }
157
+ } catch {
158
+ // Si no se puede leer, asumir que no hay tickets
159
+ }
160
+
161
+ if (hasOpenTicket || ctx.flags?.ticket_created) {
162
+ return {
163
+ passed: true,
164
+ reason: `Ticket existente verificado (${ticketCount} ticket(s) en total)`,
165
+ }
166
+ }
167
+
168
+ return {
169
+ passed: false,
170
+ reason: "No hay tickets abiertos. Todo cambio debe comenzar con un ticket.",
171
+ }
172
+ },
173
+ },
174
+
175
+ plan_approved: {
176
+ requires: "plan_approved",
177
+ message: "Debes tener un plan aprobado antes de implementar. Ejecuta: work_context_plan",
178
+ description: "Implementación requiere plan aprobado",
179
+ customCheck: (ctx) => {
180
+ const plansDir = join(process.cwd(), ".opencode", "work-context", "PLANS")
181
+ let hasPlan = false
182
+ try {
183
+ if (existsSync(plansDir)) {
184
+ const files = readdirSync(plansDir).filter((f) => f.endsWith(".md"))
185
+ // Buscar plan aprobado (últimas 24h)
186
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
187
+ for (const f of files) {
188
+ const stat = statSync(join(plansDir, f))
189
+ if (stat.mtimeMs > oneDayAgo) {
190
+ const content = readFileSync(join(plansDir, f), "utf-8")
191
+ if (
192
+ content.includes("## Plan") ||
193
+ content.toLowerCase().includes("work_context_plan")
194
+ ) {
195
+ hasPlan = true
196
+ break
197
+ }
198
+ }
199
+ }
200
+ // Si no hay plan reciente, aceptar cualquier plan
201
+ if (!hasPlan && files.length > 0) {
202
+ hasPlan = true
203
+ }
204
+ }
205
+ } catch {
206
+ // Si no se puede leer, pasar con advertencia
207
+ }
208
+
209
+ if (hasPlan || ctx.flags?.plan_approved) {
210
+ return { passed: true, reason: "Plan de trabajo verificado" }
211
+ }
212
+
213
+ return {
214
+ passed: false,
215
+ reason:
216
+ "No se encontró un plan de trabajo. Usa work_context_plan para crear uno antes de implementar.",
217
+ }
218
+ },
219
+ },
220
+
221
+ workflow_selected: {
222
+ requires: "workflow_selected",
223
+ message:
224
+ "Debes seleccionar un workflow primero. Describe la tarea con: npx openPrompt-Lang workflow select <descripción>",
225
+ description: "Requiere que se haya seleccionado un workflow automáticamente",
226
+ customCheck: (ctx) => {
227
+ if (ctx.flags?.workflow_selected) {
228
+ return {
229
+ passed: true,
230
+ reason: `Workflow seleccionado: ${ctx.flags?.selected_workflow || "desconocido"}`,
231
+ }
232
+ }
233
+
234
+ // Verificar si hay workflow seleccionado en sesión
235
+ const sessionPath = join(process.cwd(), ".opencode", "work-context", "SESSION.json")
236
+ try {
237
+ if (existsSync(sessionPath)) {
238
+ const session = JSON.parse(readFileSync(sessionPath, "utf-8"))
239
+ if (session.selectedWorkflow) {
240
+ return {
241
+ passed: true,
242
+ reason: `Workflow seleccionado en sesión: ${session.selectedWorkflow}`,
243
+ }
244
+ }
245
+ }
246
+ } catch {
247
+ // Ignorar
248
+ }
249
+
250
+ return {
251
+ passed: false,
252
+ reason:
253
+ "No hay workflow seleccionado. Usa el selector automático para elegir el workflow adecuado.",
254
+ }
255
+ },
256
+ },
78
257
  })
79
258
 
80
259
  /**