mcp-phase-gate 2.0.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/LICENSE +21 -0
- package/README.es.md +570 -0
- package/README.md +497 -0
- package/build/config.d.ts +17 -0
- package/build/config.js +136 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +389 -0
- package/build/paths.d.ts +10 -0
- package/build/paths.js +89 -0
- package/build/phases.d.ts +19 -0
- package/build/phases.js +164 -0
- package/build/tasks.d.ts +36 -0
- package/build/tasks.js +243 -0
- package/package.json +53 -0
- package/prompts/fase-1-descubrimiento.md +167 -0
- package/prompts/fase-2-decisiones.md +208 -0
- package/prompts/fase-3-plan-tecnico.md +266 -0
- package/prompts/fase-4-ejecucion.md +235 -0
- package/prompts/fase-5-auditoria-pr.md +208 -0
- package/prompts/global-rules.md +4 -0
package/build/phases.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Las fases y sus prompts. El número de fases no está cableado: se descubren de los
|
|
2
|
+
// archivos que existan en la carpeta de prompts, y cada una declara en su propia cabecera
|
|
3
|
+
// cómo se llama el documento que produce.
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { promptsLayers } from "./config.js";
|
|
7
|
+
// Las reglas globales se anteponen a cada fase.
|
|
8
|
+
const GLOBAL_RULES_FILE = "global-rules.md";
|
|
9
|
+
// Acepta "fase-1-descubrimiento.md" y también "phase-1-discovery.md",
|
|
10
|
+
// para que un juego de prompts en inglés funcione sin tocar el código.
|
|
11
|
+
export const PHASE_FILE_PATTERN = /^(?:fase|phase)-(\d+)[-.]/i;
|
|
12
|
+
// Cabecera opcional al inicio del .md, entre líneas de ---, con "clave: valor".
|
|
13
|
+
// Ahí cada fase declara cómo se llama el documento que produce. Vive pegada al prompt que
|
|
14
|
+
// describe ese documento —no en un archivo de configuración aparte— para que el nombre exista
|
|
15
|
+
// en un solo lugar y se lea en vivo, sin recompilar, igual que el resto del prompt.
|
|
16
|
+
const PHASE_FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
17
|
+
export function parsePhaseFile(fullPath) {
|
|
18
|
+
const crudo = fs.readFileSync(fullPath, "utf-8");
|
|
19
|
+
const encontrada = crudo.match(PHASE_FRONTMATTER);
|
|
20
|
+
if (!encontrada)
|
|
21
|
+
return { meta: {}, cuerpo: crudo };
|
|
22
|
+
const meta = {};
|
|
23
|
+
for (const linea of (encontrada[1] ?? "").split(/\r?\n/)) {
|
|
24
|
+
const corte = linea.indexOf(":");
|
|
25
|
+
if (corte === -1)
|
|
26
|
+
continue;
|
|
27
|
+
meta[linea.slice(0, corte).trim()] = linea
|
|
28
|
+
.slice(corte + 1)
|
|
29
|
+
.trim()
|
|
30
|
+
.replace(/^["']|["']$/g, "");
|
|
31
|
+
}
|
|
32
|
+
return { meta, cuerpo: crudo.slice(encontrada[0].length) };
|
|
33
|
+
}
|
|
34
|
+
// Descubre las fases disponibles a partir de los archivos de las carpetas de prompts.
|
|
35
|
+
// Agregar un "fase-6-*.md" basta para que exista la fase 6 — también en la carpeta propia,
|
|
36
|
+
// así que superponer no solo permite cambiar fases: permite añadirlas.
|
|
37
|
+
//
|
|
38
|
+
// Las capas se recorren en orden y la última que declare una fase es la que queda. Se
|
|
39
|
+
// superpone POR NÚMERO DE FASE, no por nombre de archivo: el usuario no tiene por qué llamar
|
|
40
|
+
// a su fase 2 igual que el paquete, y obligarle a adivinar el nombre exacto sería una trampa.
|
|
41
|
+
//
|
|
42
|
+
// `capas` es un parámetro y no una lectura directa del entorno para poder probar el
|
|
43
|
+
// comportamiento con carpetas concretas sin depender de qué traiga el paquete instalado.
|
|
44
|
+
export function discoverPhases(capas = promptsLayers()) {
|
|
45
|
+
const phases = new Map();
|
|
46
|
+
for (const base of capas) {
|
|
47
|
+
if (!fs.existsSync(base))
|
|
48
|
+
continue;
|
|
49
|
+
// Se resuelve capa por capa: dentro de una, gana el primero por orden alfabético; entre
|
|
50
|
+
// capas, gana la de más arriba. Sin este mapa intermedio la regla alfabética de la capa
|
|
51
|
+
// baja bloquearía a la de arriba, que es justo al revés de lo que se quiere.
|
|
52
|
+
const deEstaCapa = new Map();
|
|
53
|
+
for (const file of fs.readdirSync(base).sort()) {
|
|
54
|
+
const match = file.match(PHASE_FILE_PATTERN);
|
|
55
|
+
if (!match || !file.toLowerCase().endsWith(".md"))
|
|
56
|
+
continue;
|
|
57
|
+
const phaseNumber = Number(match[1]);
|
|
58
|
+
if (deEstaCapa.has(phaseNumber))
|
|
59
|
+
continue; // gana el primero por orden alfabético
|
|
60
|
+
const fullPath = path.join(base, file);
|
|
61
|
+
let meta = {};
|
|
62
|
+
let cuerpo = "";
|
|
63
|
+
try {
|
|
64
|
+
({ meta, cuerpo } = parsePhaseFile(fullPath));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Si el archivo no se puede leer aquí, el error real se reporta al invocar la fase.
|
|
68
|
+
}
|
|
69
|
+
deEstaCapa.set(phaseNumber, {
|
|
70
|
+
file: fullPath,
|
|
71
|
+
title: readPhaseTitle(cuerpo, file),
|
|
72
|
+
documento: meta.documento || undefined,
|
|
73
|
+
documentoInicial: meta.documento_inicial || undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (const [numero, datos] of deEstaCapa)
|
|
77
|
+
phases.set(numero, datos);
|
|
78
|
+
}
|
|
79
|
+
return phases;
|
|
80
|
+
}
|
|
81
|
+
// El título es el primer encabezado "# ..." del cuerpo; si no lo hay, se deriva del nombre
|
|
82
|
+
// del archivo para que la descripción de la tool siga siendo útil.
|
|
83
|
+
export function readPhaseTitle(cuerpo, fileName) {
|
|
84
|
+
for (const line of cuerpo.split(/\r?\n/)) {
|
|
85
|
+
if (line.startsWith("# "))
|
|
86
|
+
return line.slice(2).trim();
|
|
87
|
+
}
|
|
88
|
+
return fileName.replace(PHASE_FILE_PATTERN, "").replace(/\.md$/i, "");
|
|
89
|
+
}
|
|
90
|
+
// Nombre del archivo de contexto inicial que escribe start_task. Lo declara la fase 1 en su
|
|
91
|
+
// cabecera; la constante es solo el respaldo para un juego de prompts que no lo declare.
|
|
92
|
+
export const CONTEXTO_INICIAL_POR_DEFECTO = "00 - Contexto Inicial.md";
|
|
93
|
+
export function nombreContextoInicial(capas = promptsLayers()) {
|
|
94
|
+
return discoverPhases(capas).get(1)?.documentoInicial || CONTEXTO_INICIAL_POR_DEFECTO;
|
|
95
|
+
}
|
|
96
|
+
// Se lee en cada llamada (no se cachea) para poder afinar un prompt y probarlo
|
|
97
|
+
// de inmediato, sin reiniciar el servidor MCP.
|
|
98
|
+
export function loadPhasePrompt(phase, capas = promptsLayers()) {
|
|
99
|
+
const available = discoverPhases(capas);
|
|
100
|
+
const entry = available.get(phase);
|
|
101
|
+
if (!entry) {
|
|
102
|
+
const list = [...available.keys()].sort((a, b) => a - b).join(", ");
|
|
103
|
+
throw new Error(list
|
|
104
|
+
? `Fase no válida. Las fases disponibles son: ${list}.`
|
|
105
|
+
: `No se encontró ningún prompt de fase en ${capas.join(", ")}. Verifica que la carpeta 'prompts' exista junto al build.`);
|
|
106
|
+
}
|
|
107
|
+
const parts = [];
|
|
108
|
+
const globales = resolveGlobalRules(capas);
|
|
109
|
+
if (globales) {
|
|
110
|
+
parts.push(fs.readFileSync(globales, "utf-8").trim());
|
|
111
|
+
}
|
|
112
|
+
parts.push(parsePhaseFile(entry.file).cuerpo.trim());
|
|
113
|
+
const archivos = describePhaseFiles(available, phase);
|
|
114
|
+
if (archivos)
|
|
115
|
+
parts.push(archivos);
|
|
116
|
+
return parts.join("\n\n");
|
|
117
|
+
}
|
|
118
|
+
// Las reglas globales de la capa más alta que las tenga. Las tuyas SUSTITUYEN a las de
|
|
119
|
+
// fábrica, no se suman: concatenar dos juegos de reglas hace imposible saber de qué archivo
|
|
120
|
+
// salió la instrucción que estorbó cuando algo sale raro, que es justo el momento en que hay
|
|
121
|
+
// que saberlo. Si quieres las de fábrica y algo más, las copias y añades.
|
|
122
|
+
function resolveGlobalRules(capas) {
|
|
123
|
+
for (const base of [...capas].reverse()) {
|
|
124
|
+
const ruta = path.join(base, GLOBAL_RULES_FILE);
|
|
125
|
+
if (fs.existsSync(ruta))
|
|
126
|
+
return ruta;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// Bloque que se añade al final del prompt con los nombres exactos de los documentos: el que
|
|
131
|
+
// produce esta fase y los de las anteriores, que son los que hay que leer. Va aquí y no en el
|
|
132
|
+
// comando del cliente para que el nombre viva en un solo sitio; así afinar un prompt no obliga
|
|
133
|
+
// a editar además un comando en cada dispositivo.
|
|
134
|
+
export function describePhaseFiles(available, phase) {
|
|
135
|
+
const propio = available.get(phase)?.documento;
|
|
136
|
+
const previos = [...available.entries()]
|
|
137
|
+
.filter(([numero, datos]) => numero < phase && datos.documento)
|
|
138
|
+
.sort((a, b) => a[0] - b[0])
|
|
139
|
+
.map(([numero, datos]) => `- Fase ${numero}: \`${datos.documento}\``);
|
|
140
|
+
if (!propio && previos.length === 0)
|
|
141
|
+
return "";
|
|
142
|
+
const lineas = ["## ARCHIVOS DE ESTA TAREA (nombres exactos)"];
|
|
143
|
+
if (previos.length) {
|
|
144
|
+
lineas.push("", "Documentos de las fases anteriores, léelos con `read_central_doc`:", ...previos);
|
|
145
|
+
}
|
|
146
|
+
if (propio) {
|
|
147
|
+
lineas.push("", `El documento de esta fase se llama exactamente \`${propio}\`. Guárdalo con ` +
|
|
148
|
+
"`write_central_doc` en la carpeta de la tarea activa, con ese nombre y sin cambiarlo.");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
lineas.push("", "Esta fase no deja documento en la Documentación Central: su salida va en el chat.");
|
|
152
|
+
}
|
|
153
|
+
return lineas.join("\n");
|
|
154
|
+
}
|
|
155
|
+
export function describePhases() {
|
|
156
|
+
const available = discoverPhases();
|
|
157
|
+
if (available.size === 0)
|
|
158
|
+
return "No se encontraron prompts de fase.";
|
|
159
|
+
return [...available.entries()]
|
|
160
|
+
.sort((a, b) => a[0] - b[0])
|
|
161
|
+
.map(([number, { title }]) => `${number}: ${title}`)
|
|
162
|
+
.join(", ");
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=phases.js.map
|
package/build/tasks.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PhaseFile } from "./phases.js";
|
|
2
|
+
export type Punteros = Record<string, string>;
|
|
3
|
+
export declare function leerPunteros(): Punteros;
|
|
4
|
+
export declare function guardarPuntero(project: string, taskName: string): void;
|
|
5
|
+
export declare function limpiarEstadoViejo(): void;
|
|
6
|
+
export type EstadoTarea = {
|
|
7
|
+
taskName: string;
|
|
8
|
+
folderPath: string;
|
|
9
|
+
fasesHechas: number[];
|
|
10
|
+
siguienteFase: number | null;
|
|
11
|
+
fasesConDocumento: number;
|
|
12
|
+
ultimoCambio: number | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function estadoDeTarea(taskName: string, folderPath: string, phases: Map<number, PhaseFile>): EstadoTarea;
|
|
15
|
+
export declare function describeEstado(estado: EstadoTarea, phases: Map<number, PhaseFile>): string;
|
|
16
|
+
export declare function listarTareas(carpetaProyecto: string, phases: Map<number, PhaseFile>): EstadoTarea[];
|
|
17
|
+
export type TareaActiva = {
|
|
18
|
+
estado: EstadoTarea;
|
|
19
|
+
origen: "puntero" | "deducida";
|
|
20
|
+
};
|
|
21
|
+
export declare function tareaActiva(carpetaProyecto: string, phases: Map<number, PhaseFile>): TareaActiva | null;
|
|
22
|
+
export type Salto = {
|
|
23
|
+
fasePedida: number;
|
|
24
|
+
siguienteFase: number;
|
|
25
|
+
faltan: {
|
|
26
|
+
fase: number;
|
|
27
|
+
documento: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
export declare function detectarSalto(fasePedida: number, estado: EstadoTarea, phases: Map<number, PhaseFile>): Salto | null;
|
|
31
|
+
export declare function avisoDeCompuerta(fasePedida: number, phases: Map<number, PhaseFile>, opciones?: {
|
|
32
|
+
project?: string;
|
|
33
|
+
taskName?: string;
|
|
34
|
+
}): string | null;
|
|
35
|
+
export declare function avisoDeSalto(salto: Salto, taskName: string, phases: Map<number, PhaseFile>): string;
|
|
36
|
+
//# sourceMappingURL=tasks.d.ts.map
|
package/build/tasks.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Tareas: cuáles existen, en qué fase va cada una, y cuál es la activa de cada proyecto.
|
|
2
|
+
//
|
|
3
|
+
// La idea central: **la carpeta es el registro**. Las tareas son las carpetas que existen y la
|
|
4
|
+
// fase de cada una se deduce de los documentos que hay dentro, porque cada fase declara en su
|
|
5
|
+
// cabecera cómo se llama el suyo. No hay un archivo que liste tareas ni que apunte fases.
|
|
6
|
+
//
|
|
7
|
+
// Guardar esa información sería tener dos copias de la misma verdad, y la copia guardada
|
|
8
|
+
// dependería de que el modelo se acordara de reportar cada avance. Deducirla del disco la
|
|
9
|
+
// vuelve un hecho: si borras un documento a mano porque quedó mal, la fase baja sola.
|
|
10
|
+
//
|
|
11
|
+
// Lo único que sí se guarda es un puntero por proyecto —cuál es la tarea activa—, porque eso
|
|
12
|
+
// el disco no lo puede saber. Antes era un solo puntero global y dos ventanas abiertas a la
|
|
13
|
+
// vez se lo pisaban: la ventana A preguntaba dónde estaba y le contestaban la tarea de B.
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { MODULE_DIR, stateDir, stateFile } from "./config.js";
|
|
17
|
+
import { findProjectFolder, requireProjectFolder, tasksRootPath } from "./paths.js";
|
|
18
|
+
export function leerPunteros() {
|
|
19
|
+
const archivo = stateFile();
|
|
20
|
+
if (!fs.existsSync(archivo))
|
|
21
|
+
return {};
|
|
22
|
+
try {
|
|
23
|
+
const crudo = JSON.parse(fs.readFileSync(archivo, "utf-8"));
|
|
24
|
+
if (!crudo || typeof crudo !== "object" || Array.isArray(crudo))
|
|
25
|
+
return {};
|
|
26
|
+
// Se filtra en vez de confiar: el archivo lo puede haber tocado cualquiera, y una entrada
|
|
27
|
+
// con forma rara no debe tumbar las demás.
|
|
28
|
+
const punteros = {};
|
|
29
|
+
for (const [proyecto, tarea] of Object.entries(crudo)) {
|
|
30
|
+
if (typeof tarea === "string" && tarea.trim())
|
|
31
|
+
punteros[proyecto.toLowerCase()] = tarea;
|
|
32
|
+
}
|
|
33
|
+
return punteros;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Un archivo ilegible no debe dejar al usuario sin servicio: se ignora y el sistema cae
|
|
37
|
+
// en la deducción, que da una respuesta razonable sin necesitar estado guardado.
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function guardarPuntero(project, taskName) {
|
|
42
|
+
const punteros = leerPunteros(); // se relee justo antes de escribir: otra ventana pudo cambiarlo
|
|
43
|
+
punteros[project.toLowerCase()] = taskName;
|
|
44
|
+
escribirPunteros(punteros);
|
|
45
|
+
}
|
|
46
|
+
// Escribe a un temporal y renombra. El renombrado es atómico dentro del mismo sistema de
|
|
47
|
+
// archivos, así que un corte a media escritura no deja el archivo a medias: o está el de
|
|
48
|
+
// antes, o está el nuevo. Importa porque ahora son varias ventanas escribiendo el mismo
|
|
49
|
+
// archivo, y antes cada escritura era un objeto suelto que se sobrescribía entero.
|
|
50
|
+
//
|
|
51
|
+
// Queda una ventana mínima entre leer y escribir en la que dos procesos simultáneos podrían
|
|
52
|
+
// perder el puntero del otro. No se bloquea el archivo a propósito: el costo de perder un
|
|
53
|
+
// puntero es un `switch_task`, y un lock mal soltado deja el servidor inservible.
|
|
54
|
+
function escribirPunteros(punteros) {
|
|
55
|
+
const archivo = stateFile();
|
|
56
|
+
const temporal = `${archivo}.tmp`;
|
|
57
|
+
fs.mkdirSync(stateDir(), { recursive: true });
|
|
58
|
+
fs.writeFileSync(temporal, JSON.stringify(punteros, null, 2), "utf-8");
|
|
59
|
+
fs.renameSync(temporal, archivo);
|
|
60
|
+
}
|
|
61
|
+
// El formato anterior guardaba una sola tarea activa global en `active_task.json`. Ya no se
|
|
62
|
+
// puede leer —cambió de forma— y conservarlo solo deja basura en la carpeta de datos, así
|
|
63
|
+
// que se borra la primera vez que arranca esta versión.
|
|
64
|
+
export function limpiarEstadoViejo() {
|
|
65
|
+
const viejos = [path.join(stateDir(), "active_task.json"), path.join(MODULE_DIR, "active_task.json")];
|
|
66
|
+
for (const viejo of viejos) {
|
|
67
|
+
try {
|
|
68
|
+
if (fs.existsSync(viejo)) {
|
|
69
|
+
fs.unlinkSync(viejo);
|
|
70
|
+
console.error(`[orquestador] Estado en formato anterior eliminado: ${viejo}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Que no se pueda borrar no impide trabajar: el archivo viejo ya no se lee.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Mira qué documentos existen en la carpeta y traduce eso a fases hechas y fase siguiente.
|
|
79
|
+
// Solo cuentan las fases que declaran documento: una fase sin documento no puede completarse
|
|
80
|
+
// y, si contara, dejaría la deducción atascada en ella para siempre.
|
|
81
|
+
export function estadoDeTarea(taskName, folderPath, phases) {
|
|
82
|
+
const conDocumento = [...phases.entries()]
|
|
83
|
+
.filter(([, datos]) => datos.documento)
|
|
84
|
+
.sort((a, b) => a[0] - b[0]);
|
|
85
|
+
const fasesHechas = [];
|
|
86
|
+
const pendientes = [];
|
|
87
|
+
let ultimoCambio = null;
|
|
88
|
+
for (const [numero, datos] of conDocumento) {
|
|
89
|
+
const documento = path.join(folderPath, datos.documento);
|
|
90
|
+
if (!fs.existsSync(documento)) {
|
|
91
|
+
pendientes.push(numero);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
fasesHechas.push(numero);
|
|
95
|
+
try {
|
|
96
|
+
const mtime = fs.statSync(documento).mtimeMs;
|
|
97
|
+
if (ultimoCambio === null || mtime > ultimoCambio)
|
|
98
|
+
ultimoCambio = mtime;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Si no se puede leer la fecha, la tarea sigue contando como avanzada; solo pierde
|
|
102
|
+
// precisión al ordenarla por actividad.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Sin ningún documento todavía, la carpeta recién creada por start_task marca la actividad.
|
|
106
|
+
if (ultimoCambio === null) {
|
|
107
|
+
try {
|
|
108
|
+
ultimoCambio = fs.statSync(folderPath).mtimeMs;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
ultimoCambio = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
taskName,
|
|
116
|
+
folderPath,
|
|
117
|
+
fasesHechas,
|
|
118
|
+
siguienteFase: pendientes.length ? pendientes[0] : null,
|
|
119
|
+
fasesConDocumento: conDocumento.length,
|
|
120
|
+
ultimoCambio,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Frase corta para el chat. Se le pasa el mapa de fases para poder nombrar la fase siguiente
|
|
124
|
+
// con su título real en vez de un número suelto.
|
|
125
|
+
export function describeEstado(estado, phases) {
|
|
126
|
+
if (estado.fasesConDocumento === 0) {
|
|
127
|
+
return "fase desconocida (ningún prompt de fase declara documento, así que no hay nada que deducir)";
|
|
128
|
+
}
|
|
129
|
+
if (estado.siguienteFase === null) {
|
|
130
|
+
return "terminada: todas las fases dejaron su documento";
|
|
131
|
+
}
|
|
132
|
+
const titulo = phases.get(estado.siguienteFase)?.title;
|
|
133
|
+
const siguiente = titulo
|
|
134
|
+
? `va en Fase ${estado.siguienteFase} (${titulo})`
|
|
135
|
+
: `va en Fase ${estado.siguienteFase}`;
|
|
136
|
+
return estado.fasesHechas.length
|
|
137
|
+
? `${siguiente}; hechas: ${estado.fasesHechas.join(", ")}`
|
|
138
|
+
: `${siguiente}; todavía sin documentos`;
|
|
139
|
+
}
|
|
140
|
+
// ─── Listado ────────────────────────────────────────────────────────────────
|
|
141
|
+
// Las tareas son las carpetas que hay bajo la subcarpeta de tareas del proyecto. Se listan
|
|
142
|
+
// del disco, así que también aparecen las que alguien creó a mano sin pasar por start_task.
|
|
143
|
+
export function listarTareas(carpetaProyecto, phases) {
|
|
144
|
+
const raiz = tasksRootPath(carpetaProyecto);
|
|
145
|
+
if (!fs.existsSync(raiz))
|
|
146
|
+
return [];
|
|
147
|
+
return fs
|
|
148
|
+
.readdirSync(raiz, { withFileTypes: true })
|
|
149
|
+
.filter((entrada) => entrada.isDirectory())
|
|
150
|
+
.map((entrada) => estadoDeTarea(entrada.name, path.join(raiz, entrada.name), phases))
|
|
151
|
+
.sort((a, b) => (b.ultimoCambio ?? 0) - (a.ultimoCambio ?? 0));
|
|
152
|
+
}
|
|
153
|
+
// Con puntero guardado se devuelve esa. Sin puntero —estado borrado, equipo nuevo, archivo
|
|
154
|
+
// corrupto— se deduce la tarea con actividad más reciente, marcada como deducción. Así perder
|
|
155
|
+
// el archivo de estado deja de ser un callejón sin salida.
|
|
156
|
+
export function tareaActiva(carpetaProyecto, phases) {
|
|
157
|
+
const tareas = listarTareas(carpetaProyecto, phases);
|
|
158
|
+
if (tareas.length === 0)
|
|
159
|
+
return null;
|
|
160
|
+
const apuntada = leerPunteros()[carpetaProyecto.toLowerCase()];
|
|
161
|
+
if (apuntada) {
|
|
162
|
+
const encontrada = tareas.find((tarea) => tarea.taskName === apuntada);
|
|
163
|
+
// Un puntero a una carpeta que ya no existe (renombrada o borrada) no debe dejar sin
|
|
164
|
+
// respuesta: se ignora y se deduce, igual que si no hubiera puntero.
|
|
165
|
+
if (encontrada)
|
|
166
|
+
return { estado: encontrada, origen: "puntero" };
|
|
167
|
+
}
|
|
168
|
+
return { estado: tareas[0], origen: "deducida" };
|
|
169
|
+
}
|
|
170
|
+
export function detectarSalto(fasePedida, estado, phases) {
|
|
171
|
+
// Sin fases con documento no hay nada contra lo que comparar; y una tarea terminada no
|
|
172
|
+
// tiene huecos, así que ninguna fase que se pida puede ser un salto.
|
|
173
|
+
if (estado.siguienteFase === null)
|
|
174
|
+
return null;
|
|
175
|
+
// Pedir la fase que toca, o una anterior, es legítimo. Solo se mira hacia adelante.
|
|
176
|
+
if (fasePedida <= estado.siguienteFase)
|
|
177
|
+
return null;
|
|
178
|
+
const faltan = [...phases.entries()]
|
|
179
|
+
.filter(([numero, datos]) => numero < fasePedida && datos.documento && !estado.fasesHechas.includes(numero))
|
|
180
|
+
.sort((a, b) => a[0] - b[0])
|
|
181
|
+
.map(([numero, datos]) => ({ fase: numero, documento: datos.documento }));
|
|
182
|
+
return { fasePedida, siguienteFase: estado.siguienteFase, faltan };
|
|
183
|
+
}
|
|
184
|
+
// El aviso va al principio del prompt, no en un campo aparte, porque lo tiene que leer el
|
|
185
|
+
// modelo como parte de sus instrucciones. Avisa y entrega igual: bloquear estorbaría el día
|
|
186
|
+
// que haga falta saltarse una fase a propósito, y hoy no hay dato de cuántas veces pasa.
|
|
187
|
+
// Resuelve de qué tarea se está hablando y devuelve el aviso, o null si no hay nada que decir.
|
|
188
|
+
//
|
|
189
|
+
// Quién hace este trabajo es la decisión de diseño de la compuerta: lo hace el servidor, no el
|
|
190
|
+
// modelo. El puntero por proyecto ya está guardado aquí, así que pedírselo al modelo sería
|
|
191
|
+
// trabajo de más y una vía de fricción — si no lograra deducirlo acabaría preguntándoselo al
|
|
192
|
+
// usuario en cada fase, y escribir "/f4" para que te contesten "¿en qué tarea estamos?" es peor
|
|
193
|
+
// que no tener compuerta.
|
|
194
|
+
//
|
|
195
|
+
// `project` y `taskName` son solo anulación manual, para comprobar contra una tarea distinta de
|
|
196
|
+
// la activa. Sin ellos se usa el puntero; y cuando de verdad no se puede saber se dice en voz
|
|
197
|
+
// alta, en vez de callar como si todo estuviera en orden.
|
|
198
|
+
export function avisoDeCompuerta(fasePedida, phases, opciones = {}) {
|
|
199
|
+
let carpetaProyecto;
|
|
200
|
+
if (opciones.project) {
|
|
201
|
+
carpetaProyecto = requireProjectFolder(opciones.project);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Sin proyecto solo se puede resolver si hay exactamente un candidato con tarea activa.
|
|
205
|
+
const conPuntero = Object.keys(leerPunteros());
|
|
206
|
+
if (conPuntero.length === 0)
|
|
207
|
+
return null; // Nada empezado: no hay nada que comprobar.
|
|
208
|
+
if (conPuntero.length > 1) {
|
|
209
|
+
return ("> Nota: no se comprobó la compuerta de fase porque no se indicó `project` y hay varios " +
|
|
210
|
+
"proyectos con tarea activa. Pasa `project` para que se compruebe.");
|
|
211
|
+
}
|
|
212
|
+
const encontrada = findProjectFolder(conPuntero[0]);
|
|
213
|
+
if (!encontrada)
|
|
214
|
+
return null; // Puntero de un proyecto que ya no existe: se ignora.
|
|
215
|
+
carpetaProyecto = encontrada;
|
|
216
|
+
}
|
|
217
|
+
const estado = opciones.taskName
|
|
218
|
+
? listarTareas(carpetaProyecto, phases).find((tarea) => tarea.taskName === opciones.taskName)
|
|
219
|
+
: tareaActiva(carpetaProyecto, phases)?.estado;
|
|
220
|
+
if (!estado)
|
|
221
|
+
return null; // Proyecto sin tareas, o un nombre de tarea que no existe.
|
|
222
|
+
const salto = detectarSalto(fasePedida, estado, phases);
|
|
223
|
+
return salto ? avisoDeSalto(salto, estado.taskName, phases) : null;
|
|
224
|
+
}
|
|
225
|
+
export function avisoDeSalto(salto, taskName, phases) {
|
|
226
|
+
const titulo = phases.get(salto.siguienteFase)?.title;
|
|
227
|
+
const vaEn = titulo ? `Fase ${salto.siguienteFase} (${titulo})` : `Fase ${salto.siguienteFase}`;
|
|
228
|
+
const lista = salto.faltan.map(({ fase, documento }) => `- Fase ${fase}: falta \`${documento}\``);
|
|
229
|
+
return [
|
|
230
|
+
"## ATENCIÓN — COMPUERTA DE FASE",
|
|
231
|
+
"",
|
|
232
|
+
`Pediste la Fase ${salto.fasePedida}, pero la tarea "${taskName}" no tiene los documentos de fases anteriores:`,
|
|
233
|
+
"",
|
|
234
|
+
...lista,
|
|
235
|
+
"",
|
|
236
|
+
`Esta tarea va en la ${vaEn}. NO hagas el trabajo de la Fase ${salto.fasePedida} sobre documentos que no existen ` +
|
|
237
|
+
"ni te los inventes a partir del contexto del chat.",
|
|
238
|
+
"",
|
|
239
|
+
"Díselo al usuario antes de nada y pregúntale si prefiere hacer primero la fase que falta o continuar de todos " +
|
|
240
|
+
"modos. Lo que sigue es el prompt de la fase que pediste: aplícalo solo después de resolver esto con el usuario.",
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=tasks.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-phase-gate",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Servidor MCP que parte cada requerimiento en cinco fases con compuerta y obliga a que cada una deje su documento en una documentación central.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-phase-gate": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build",
|
|
11
|
+
"prompts",
|
|
12
|
+
"!build/**/*.map"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node build/index.js",
|
|
20
|
+
"dev": "ts-node src/index.ts",
|
|
21
|
+
"test": "tsc -p tsconfig.test.json && node --test \"build-test/test/**/*.test.js\"",
|
|
22
|
+
"prepublishOnly": "npm run build && npm test"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Montse2308/mcp-phase-gate.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"phase-gate",
|
|
32
|
+
"workflow",
|
|
33
|
+
"documentation",
|
|
34
|
+
"cursor",
|
|
35
|
+
"claude-code"
|
|
36
|
+
],
|
|
37
|
+
"author": "Montserrat Ximena Hernández Gallegos",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"type": "module",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Montse2308/mcp-phase-gate/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Montse2308/mcp-phase-gate#readme",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"dotenv": "^17.4.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.9.1",
|
|
50
|
+
"ts-node": "^10.9.2",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
documento: 01 - Análisis Técnico.md
|
|
3
|
+
documento_inicial: 00 - Contexto Inicial.md
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Descubrimiento
|
|
7
|
+
|
|
8
|
+
## ROL DE COMPORTAMIENTO (GLOBAL)
|
|
9
|
+
Eres un Arquitecto de Software y Analista de Sistemas Senior. Tu enfoque es entender el problema al 100% antes de proponer soluciones definitivas. Tu prioridad es la investigación profunda y el pensamiento crítico.
|
|
10
|
+
|
|
11
|
+
## REGLAS DE LA FASE 1 (DESCUBRIMIENTO)
|
|
12
|
+
1. NO escribas código de producción.
|
|
13
|
+
2. Lee el código real y la documentación central antes de opinar. Cada afirmación del
|
|
14
|
+
documento tiene que poder señalarse en un archivo concreto.
|
|
15
|
+
3. **La investigación se hace AQUÍ, no se difiere.** Ver la sección VERIFICACIÓN.
|
|
16
|
+
4. Genera el documento de análisis en la carpeta de la tarea (ej. `01 - Análisis Técnico.md`)
|
|
17
|
+
siguiendo el contrato de estructura de abajo.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ALCANCE DE ESTA FASE (se presta a confusión: léelo)
|
|
22
|
+
|
|
23
|
+
La Fase 1 entrega **diagnóstico más una recomendación de dirección marcada como tal**.
|
|
24
|
+
No entrega plan de implementación.
|
|
25
|
+
|
|
26
|
+
| Sí te toca | No te toca |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Inventariar qué existe y quién lo consume, nombrando archivos y líneas | Listar los archivos "a modificar" o los pasos de implementación → **Fase 3** |
|
|
29
|
+
| Explicar la causa raíz y demostrarla | Definir qué se va a validar → **Fase 2**; convertirlo en criterios verificables → **Fase 3** |
|
|
30
|
+
| Recomendar una dirección, diciendo explícitamente que es una recomendación | Escribir pasos de prueba en la interfaz → **Fase 4** |
|
|
31
|
+
| Listar las decisiones que hay que tomar | Tomar esas decisiones → **Fase 2** |
|
|
32
|
+
|
|
33
|
+
La línea es esta: **describir lo que existe es Fase 1; comprometerse a lo que se va a
|
|
34
|
+
cambiar es Fase 3.** Nombrar `app/Models/Ejemplo.php:112` explicando qué hace ahí es
|
|
35
|
+
Fase 1. Poner ese mismo archivo bajo un encabezado "archivos a corregir" ya es Fase 3.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## VERIFICACIÓN: HAZLA EN ESTA FASE
|
|
40
|
+
|
|
41
|
+
Si una hipótesis se puede comprobar ejecutando algo, **ejecútalo ahora** y reporta la
|
|
42
|
+
salida real: una consulta contra una réplica de solo lectura, una sesión de consola del
|
|
43
|
+
framework, recuperar una versión anterior de un archivo con `git show` para comparar
|
|
44
|
+
comportamientos, una medición cronometrada.
|
|
45
|
+
|
|
46
|
+
Un diagnóstico medido vale mucho más que uno argumentado. Descartar una hipótesis
|
|
47
|
+
"porque no tiene sentido" no sirve; descartarla porque la mediste sí.
|
|
48
|
+
|
|
49
|
+
Dejar consultas escritas "listas para correr" es el **último recurso**, válido solo
|
|
50
|
+
cuando el acceso no existe de verdad. Si caes ahí:
|
|
51
|
+
- Dilo como una **laguna del análisis**, no como un entregable.
|
|
52
|
+
- Pide el acceso o pide que se corran **antes** de dar la fase por cerrada.
|
|
53
|
+
|
|
54
|
+
Si te equivocaste al medir, dilo y descarta esos números explícitamente. Un análisis que
|
|
55
|
+
confiesa su propio error de método es en el que se puede confiar.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ESTRUCTURA DEL DOCUMENTO
|
|
60
|
+
|
|
61
|
+
### Núcleo (siempre, en este orden)
|
|
62
|
+
|
|
63
|
+
1. **Encabezado de metadatos.** Tarea, proyecto, fase, ruta o módulo afectado, fecha.
|
|
64
|
+
Declara que no se escribió código de producción.
|
|
65
|
+
2. **Resumen ejecutivo.** VA ARRIBA, nunca al final. El veredicto en la primera línea, con
|
|
66
|
+
el número concreto si lo hay. Quien lea solo esta sección debe quedarse con lo esencial.
|
|
67
|
+
3. **Estado actual / mapa de código.** Cómo funciona hoy, con `archivo:línea`.
|
|
68
|
+
4. **Hallazgos.** Cada uno etiquetado con **Severidad** (Alta / Media / Baja) y
|
|
69
|
+
**Confianza** (Alta / Media / Baja). Separa el hallazgo principal de los secundarios.
|
|
70
|
+
5. **Impacto y superficie afectada.** Quién más consume lo que se tocaría, y en qué estado
|
|
71
|
+
está cada consumidor. Una tabla suele ser lo más claro.
|
|
72
|
+
6. **Lo que NO se debe tocar**, con el porqué de cada punto. Es la regla de Cero Rupturas
|
|
73
|
+
hecha sección: si algo es tentador de cambiar pero es compartido, dilo aquí.
|
|
74
|
+
7. **Decisiones para la Fase 2.** Formato `D1`, `D2`, … Cada decisión con sus opciones
|
|
75
|
+
`(a) (b) (c)` y una recomendación técnica marcada como tal. La Fase 2 debe poder
|
|
76
|
+
resolverse escogiendo, no redactando.
|
|
77
|
+
|
|
78
|
+
### Secciones condicionales
|
|
79
|
+
|
|
80
|
+
Incluye SOLO las que cumplan su disparador. Un ajuste chico no dispara casi ninguna y el
|
|
81
|
+
documento sale corto solo: así es como se ajusta la profundidad, no recortando el análisis.
|
|
82
|
+
|
|
83
|
+
| Sección | Se activa si… |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Comportamiento observado | el ticket reporta un síntoma concreto: qué se ve y dónde |
|
|
86
|
+
| Respuesta a las hipótesis del ticket | quien reportó planteó causas posibles. Contéstalas UNA POR UNA, incluso las que descartas |
|
|
87
|
+
| Reproducción del síntoma | lograste reproducirlo. Incluye el escenario numérico exacto |
|
|
88
|
+
| Cómo se verificó / metodología | hubo medición o ejecución. Debe ser reproducible por otro |
|
|
89
|
+
| Requisitos funcionales (`RF1..RFn`) | es funcionalidad nueva y el requerimiento es difuso |
|
|
90
|
+
| Opciones evaluadas | hay más de un camino viable |
|
|
91
|
+
| Validaciones pendientes | no hubo acceso para comprobar algo. Es una laguna, márcala como tal |
|
|
92
|
+
| Deuda técnica fuera de alcance | encontraste algo real pero ajeno al ticket |
|
|
93
|
+
| Predicción verificable | el diagnóstico implica algo futuro comprobable |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## REGLAS DE REDACCIÓN
|
|
98
|
+
|
|
99
|
+
- **Siempre `archivo:línea`.** Nunca hables de "el servicio de cálculo" sin decir cuál es.
|
|
100
|
+
- **Sin emojis**, en ninguna sección.
|
|
101
|
+
- **Los hallazgos secundarios van como lista de una línea cada uno**, con la nota de que
|
|
102
|
+
se detallan si se aprueban. Excepción: si un hallazgo secundario tiene Severidad Alta,
|
|
103
|
+
ese sí va completo — no escondas algo grave en una lista.
|
|
104
|
+
- Si el requerimiento parte de una **premisa falsa**, dilo lo más arriba posible. Descubrir
|
|
105
|
+
en la Fase 4 que el supuesto del ticket no existe en el código es el fallo más caro
|
|
106
|
+
de todo el flujo.
|
|
107
|
+
- Cuando descartes una opción, **reconoce primero su ventaja** y luego explica qué la mata.
|
|
108
|
+
Una opción descartada sin argumento parece un descuido.
|
|
109
|
+
- Si algo va a morder a quien implemente, déjaselo escrito de forma directa.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## PROHIBIDO
|
|
114
|
+
|
|
115
|
+
- Afirmar cualquier cosa que no puedas señalar en un archivo o en una salida real.
|
|
116
|
+
- Encabezados de "archivos a modificar" o "pasos de implementación": eso es Fase 3.
|
|
117
|
+
- Listas de qué validar o criterios de aceptación: eso es de la Fase 2 en adelante.
|
|
118
|
+
- Pasos de prueba en la interfaz: eso es Fase 4.
|
|
119
|
+
- Tomar las decisiones en vez de listarlas: eso es Fase 2.
|
|
120
|
+
- Volver a escribir sobre este documento en fases posteriores. La decisión que se tome en
|
|
121
|
+
la Fase 2 vive en el documento de la Fase 2, no aquí.
|
|
122
|
+
- Rellenar secciones condicionales que no aplican, con tal de que el documento se vea completo.
|
|
123
|
+
- Inventar números o estimar tiempos que no mediste.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## MARCAS DE CALIDAD
|
|
128
|
+
|
|
129
|
+
Así se ve un análisis que sirve. Son ejemplos reales de documentos aprobados.
|
|
130
|
+
|
|
131
|
+
**El veredicto arriba y con número, no con adjetivos:**
|
|
132
|
+
> "El tablero tarda ~66 segundos por request y el costo se concentra en una sola
|
|
133
|
+
> subconsulta, no en el diseño general ni en los cambios recientes."
|
|
134
|
+
|
|
135
|
+
**Las hipótesis del ticket, contestadas una por una:**
|
|
136
|
+
> "¿Fue la columna nueva? NO. ¿Fue el widget? NO. ¿Es un problema general del sistema? NO.
|
|
137
|
+
> Las tres se descartaron con medición, no con opinión."
|
|
138
|
+
|
|
139
|
+
**El propio error de método, confesado:**
|
|
140
|
+
> "Un primer intento de medición pegó por error a la conexión default y midió solo el
|
|
141
|
+
> costo fijo. Esos números se descartaron."
|
|
142
|
+
|
|
143
|
+
**La premisa falsa del requerimiento, marcada de entrada:**
|
|
144
|
+
> "Estas dos afirmaciones NO describen el mismo dato en el código actual. Es el punto que
|
|
145
|
+
> hay que resolver antes de diseñar."
|
|
146
|
+
|
|
147
|
+
**Una opción descartada, reconociendo su ventaja:**
|
|
148
|
+
> "Para este caso sería viable y más limpio conceptualmente. Descartada porque no se puede
|
|
149
|
+
> aplicar al segundo problema, y dejaría dos soluciones distintas para el mismo defecto en
|
|
150
|
+
> el mismo archivo."
|
|
151
|
+
|
|
152
|
+
**Una predicción falsable:**
|
|
153
|
+
> "Mejorará solo durante el mes conforme se completen los registros, y volverá a colapsar
|
|
154
|
+
> el día 5 del mes siguiente."
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## AUTOVERIFICACIÓN (antes de entregar, revísalas una por una)
|
|
159
|
+
|
|
160
|
+
1. ¿El resumen ejecutivo está arriba y trae el veredicto con su número?
|
|
161
|
+
2. ¿Cada hallazgo tiene Severidad y Confianza?
|
|
162
|
+
3. ¿Toda afirmación se puede señalar en un archivo concreto o en una salida real?
|
|
163
|
+
4. ¿Comprobé lo que era comprobable, en vez de dejarlo pendiente?
|
|
164
|
+
5. ¿Cada sección condicional cumple su disparador? Si no, bórrala.
|
|
165
|
+
6. ¿Me metí en terreno de la Fase 3 (archivos a modificar, criterios de aceptación) o de
|
|
166
|
+
la Fase 4 (pasos de prueba)?
|
|
167
|
+
7. ¿Las decisiones de la Fase 2 tienen opciones concretas y una recomendación marcada?
|